feat: Add domain option to cookie commands (#24471)

This commit is contained in:
Chris Breiding
2022-11-04 14:32:59 -04:00
committed by GitHub
parent 5f472efe75
commit bf2fc3a848
10 changed files with 672 additions and 197 deletions

View File

@@ -775,20 +775,20 @@ declare namespace Cypress {
clear(options?: Partial<ClearOptions>): Chainable<Subject>
/**
* Clear a specific browser cookie.
* Clear a specific browser cookie for the current superdomain 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.
*
* @see https://on.cypress.io/clearcookie
*/
clearCookie(name: string, options?: Partial<Loggable & Timeoutable>): Chainable<null>
clearCookie(name: string, options?: CookieOptions): Chainable<null>
/**
* Clear all browser cookies.
* 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 all browser cookies for the current superdomain 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.
*
* @see https://on.cypress.io/clearcookies
*/
clearCookies(options?: Partial<Loggable & Timeoutable>): Chainable<null>
clearCookies(options?: CookieOptions): Chainable<null>
/**
* Clear data in local storage.
@@ -1248,18 +1248,18 @@ declare namespace Cypress {
get<S = any>(alias: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<S>
/**
* Get a browser cookie by its name.
* Get a browser cookie by its name for the current superdomain or for the domain specified.
*
* @see https://on.cypress.io/getcookie
*/
getCookie(name: string, options?: Partial<Loggable & Timeoutable>): Chainable<Cookie | null>
getCookie(name: string, options?: CookieOptions): Chainable<Cookie | null>
/**
* Get all of the browser cookies.
* Get all of the browser cookies for the current superdomain or for the domain specified.
*
* @see https://on.cypress.io/getcookies
*/
getCookies(options?: Partial<Loggable & Timeoutable>): Chainable<Cookie[]>
getCookies(options?: CookieOptions): Chainable<Cookie[]>
/**
* Navigate back or forward to the previous or next URL in the browser's history.
@@ -2633,6 +2633,14 @@ declare namespace Cypress {
cmdKey: boolean
}
interface CookieOptions extends Partial<Loggable & Timeoutable> {
/**
* Domain to set cookies on or get cookies from
* @default superdomain of the current app under test
*/
domain?: string
}
interface PEMCert {
/**
* Path to the certificate file, relative to project root.

View File

@@ -936,6 +936,7 @@ namespace CypressSessionsTests {
validate: { foo: true } // $ExpectError
})
}
namespace CypressCurrentTest {
Cypress.currentTest.title // $ExpectType string
Cypress.currentTest.titlePath // $ExpectType string[]
@@ -974,3 +975,82 @@ namespace CypressOriginTests {
cy.origin('example.com', { args: ['value'] }, (value: boolean[]) => {}) // $ExpectError
cy.origin('example.com', {}, (value: undefined) => {}) // $ExpectError
}
namespace CypressGetCookiesTests {
cy.getCookies().then((cookies) => {
cookies // $ExpectType Cookie[]
})
cy.getCookies({ log: true })
cy.getCookies({ timeout: 10 })
cy.getCookies({ domain: 'localhost' })
cy.getCookies({ log: true, timeout: 10, domain: 'localhost' })
cy.getCookies({ log: 'true' }) // $ExpectError
cy.getCookies({ timeout: '10' }) // $ExpectError
cy.getCookies({ domain: false }) // $ExpectError
}
namespace CypressGetCookieTests {
cy.getCookie('name').then((cookie) => {
cookie // $ExpectType Cookie | null
})
cy.getCookie('name', { log: true })
cy.getCookie('name', { timeout: 10 })
cy.getCookie('name', { domain: 'localhost' })
cy.getCookie('name', { log: true, timeout: 10, domain: 'localhost' })
cy.getCookie('name', { log: 'true' }) // $ExpectError
cy.getCookie('name', { timeout: '10' }) // $ExpectError
cy.getCookie('name', { domain: false }) // $ExpectError
}
namespace CypressSetCookieTests {
cy.setCookie('name', 'value').then((cookie) => {
cookie // $ExpectType Cookie
})
cy.setCookie('name', 'value', { log: true })
cy.setCookie('name', 'value', { timeout: 10 })
cy.setCookie('name', 'value', {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: false,
expiry: 12345,
sameSite: 'lax',
})
cy.setCookie('name', 'value', { log: true, timeout: 10, domain: 'localhost' })
cy.setCookie('name') // $ExpectError
cy.setCookie('name', 'value', { log: 'true' }) // $ExpectError
cy.setCookie('name', 'value', { timeout: '10' }) // $ExpectError
cy.setCookie('name', 'value', { domain: false }) // $ExpectError
cy.setCookie('name', 'value', { foo: 'bar' }) // $ExpectError
}
namespace CypressClearCookieTests {
cy.clearCookie('name').then((result) => {
result // $ExpectType null
})
cy.clearCookie('name', { log: true })
cy.clearCookie('name', { timeout: 10 })
cy.clearCookie('name', { domain: 'localhost' })
cy.clearCookie('name', { log: true, timeout: 10, domain: 'localhost' })
cy.clearCookie('name', { log: 'true' }) // $ExpectError
cy.clearCookie('name', { timeout: '10' }) // $ExpectError
cy.clearCookie('name', { domain: false }) // $ExpectError
}
namespace CypressClearCookiesTests {
cy.clearCookies().then((result) => {
result // $ExpectType null
})
cy.clearCookies({ log: true })
cy.clearCookies({ timeout: 10 })
cy.clearCookies({ domain: 'localhost' })
cy.clearCookies({ log: true, timeout: 10, domain: 'localhost' })
cy.clearCookies({ log: 'true' }) // $ExpectError
cy.clearCookies({ timeout: '10' }) // $ExpectError
cy.clearCookies({ domain: false }) // $ExpectError
}

View File

@@ -2,21 +2,335 @@ const { assertLogLength } = require('../../support/utils')
const { stripIndent } = require('common-tags')
const { Promise } = Cypress
const isWebkit = Cypress.isBrowser('webkit')
describe('src/cy/commands/cookies - no stub', () => {
it('clears all cookies', () => {
cy.setCookie('foo', 'bar')
cy.getCookies().should('have.length', 1)
cy.clearCookies()
cy.getCookies().should('have.length', 0)
context('#getCookies', () => {
// this can be removed along with the experimental flag since once the flag
// removed, clearing cookies for all domains will be done by default
beforeEach(() => {
if (!Cypress.config('experimentalSessionAndOrigin')) {
cy.clearCookies({ domain: null })
}
})
it('returns cookies from the domain matching the AUT by default', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('baz', 'qux', { domain: 'foobar.com' })
cy.setCookie('foo', 'bar') // defaults to (super)domain: barbaz.com
cy.setCookie('qux', 'quuz', { domain: 'www.barbaz.com' })
cy.getCookies().then((cookies) => {
expect(cookies).to.have.length(2)
// both the barbaz.com and www.barbaz.com cookies are yielded
expect(cookies[0].domain).to.match(/\.?barbaz\.com/)
expect(cookies[1].domain).to.match(/\.?www\.barbaz\.com/)
})
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.getCookies().then((cookies) => {
expect(cookies[0].domain).to.match(/\.?www\.foobar\.com/)
expect(cookies[1].domain).to.match(/\.?foobar\.com/)
})
})
})
it('returns cookies for the specified domain', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('foo', 'bar') // defaults to (super)domain: barbaz.com
cy.setCookie('qux', 'quuz', { domain: 'www.barbaz.com' })
cy.getCookies({ domain: 'www.foobar.com' }).then((cookies) => {
expect(cookies[0].domain).to.match(/\.?www\.foobar\.com/)
})
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.getCookies({ domain: 'barbaz.com' }).then((cookies) => {
expect(cookies).to.have.length(2)
// both the barbaz.com and www.barbaz.com cookies are yielded
expect(cookies[0].domain).to.match(/\.?barbaz\.com/)
expect(cookies[1].domain).to.match(/\.?www\.barbaz\.com/)
})
})
})
it('returns cookies for all domains when domain is null', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('foo', 'bar')
cy.getCookies({ domain: null }).should('have.length', 2)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.getCookies({ domain: null }).should('have.length', 2)
})
})
})
it('clears a single cookie', () => {
cy.setCookie('foo', 'bar')
cy.setCookie('key', 'val')
cy.getCookies().should('have.length', 2)
cy.clearCookie('foo')
cy.getCookies().should('have.length', 1).then((cookies) => {
expect(cookies[0].name).to.eq('key')
context('#getCookie', () => {
it('returns the cookie from the domain matching the AUT by default', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('foo', 'bar')
cy.getCookie('foo').its('domain').should('match', /\.?barbaz\.com/)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.getCookie('foo').its('domain').should('match', /\.?www\.foobar\.com/)
})
})
it('returns the cookie from the specified domain', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('foo', 'bar')
cy.getCookie('foo', { domain: 'www.foobar.com' })
.its('domain').should('match', /\.?www\.foobar\.com/)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.getCookie('foo', { domain: 'barbaz.com' })
.its('domain').should('match', /\.?barbaz\.com/)
})
})
it('returns cookie for any domain when domain is null', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.getCookie('foo', { domain: null })
.its('domain').should('match', /\.?www\.foobar\.com/)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.clearCookie('foo')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.getCookie('foo', { domain: null })
.its('domain').should('match', /\.?barbaz\.com/)
})
})
})
context('#setCookie', () => {
it('sets the cookie on the domain matching the AUT by default', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.getCookie('foo').its('domain').should('match', /\.?barbaz\.com/)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.getCookie('foo').its('domain').should('equal', '.foobar.com')
})
})
it('set the cookie on the specified domain', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.getCookie('foo', { domain: 'www.foobar.com' })
.its('domain').should('match', /\.?www\.foobar\.com/)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.getCookie('foo', { domain: 'barbaz.com' })
.its('domain').should('match', /\.?barbaz\.com/)
})
})
})
context('#clearCookies', () => {
it('clears all cookies', () => {
cy.setCookie('foo', 'bar')
cy.getCookies().should('have.length', 1)
cy.clearCookies()
cy.getCookies().should('have.length', 0)
})
it('clears the cookies on the domain matching the AUT by default', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('baz', 'qux')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.clearCookies()
cy.getCookie('foo').should('be.null')
cy.getCookie('baz').should('be.null')
cy.getCookie('foo', { domain: 'www.foobar.com' }).should('exist')
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('baz', 'qux')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.clearCookies()
cy.getCookie('foo').should('be.null')
cy.getCookie('baz').should('be.null')
cy.getCookie('foo', { domain: 'barbaz.com' }).should('exist')
})
})
it('clears the cookies on the specified domain', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.setCookie('baz', 'qux', { domain: 'www.foobar.com' })
cy.clearCookies({ domain: 'www.foobar.com' })
cy.getCookie('foo', { domain: 'www.foobar.com' }).should('be.null')
cy.getCookie('baz', { domain: 'www.foobar.com' }).should('be.null')
cy.getCookie('foo').should('exist')
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('baz', 'qux', { domain: 'barbaz.com' })
cy.clearCookies({ domain: 'barbaz.com' })
cy.getCookie('foo', { domain: 'barbaz.com' }).should('be.null')
cy.getCookie('baz', { domain: 'barbaz.com' }).should('be.null')
cy.getCookie('foo').should('exist')
})
})
it('clears cookies for all domains when domain is null', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.clearCookies({ domain: null })
cy.getCookies().should('have.length', 0)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.clearCookies({ domain: null })
cy.getCookies().should('have.length', 0)
})
})
})
context('#clearCookie', () => {
it('clears a single cookie', () => {
cy.setCookie('foo', 'bar')
cy.setCookie('key', 'val')
cy.getCookies().should('have.length', 2)
cy.clearCookie('foo')
cy.getCookies().should('have.length', 1).then((cookies) => {
expect(cookies[0].name).to.eq('key')
})
})
it('clears the cookie on the domain matching the AUT by default', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.clearCookie('foo')
cy.getCookie('foo').should('be.null')
cy.getCookie('foo', { domain: 'www.foobar.com' }).should('exist')
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.clearCookie('foo')
cy.getCookie('foo').should('be.null')
cy.getCookie('foo', { domain: 'barbaz.com' }).should('exist')
})
})
it('clears the cookie on the specified domain', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.clearCookie('foo', { domain: 'www.foobar.com' })
cy.getCookie('foo', { domain: 'www.foobar.com' }).should('be.null')
cy.getCookie('foo').should('exist')
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar')
cy.clearCookie('foo', { domain: 'barbaz.com' })
cy.getCookie('foo', { domain: 'barbaz.com' }).should('be.null')
cy.getCookie('foo').should('exist')
})
})
})
it('clears cookie for any domain when domain is null', () => {
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'www.foobar.com' })
cy.clearCookie('foo', { domain: null })
cy.getCookies().should('have.length', 0)
if (isWebkit || !Cypress.config('experimentalSessionAndOrigin')) return
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/fixtures/generic.html')
cy.setCookie('foo', 'bar', { domain: 'barbaz.com' })
cy.clearCookie('foo', { domain: null })
cy.getCookies().should('have.length', 0)
})
})
})
@@ -25,6 +339,8 @@ describe('src/cy/commands/cookies', () => {
beforeEach(() => {
// call through normally on everything
cy.stub(Cypress, 'automation').rejects(new Error('Cypress.automation was not stubbed'))
cy.visit('http://localhost:3500/fixtures/generic.html')
})
context('test:before:run:async', () => {
@@ -143,6 +459,20 @@ describe('src/cy/commands/cookies', () => {
return null
})
it('when an invalid domain prop is supplied', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
expect(lastLog.get('error').message).to.eq('`cy.getCookies()` must be passed a valid domain name. You passed: `true`')
expect(lastLog.get('error').docsUrl).to.eq('https://on.cypress.io/getcookies')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.getCookies({ domain: true })
})
it('logs once on error', function (done) {
const error = new Error('some err message')
@@ -370,6 +700,20 @@ describe('src/cy/commands/cookies', () => {
cy.getCookie(123)
})
it('when an invalid domain prop is supplied', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
expect(lastLog.get('error').message).to.eq('`cy.getCookie()` must be passed a valid domain name. You passed: `true`')
expect(lastLog.get('error').docsUrl).to.eq('https://on.cypress.io/getcookie')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.getCookie('foo', { domain: true })
})
})
describe('.log', () => {
@@ -454,8 +798,7 @@ describe('src/cy/commands/cookies', () => {
})
})
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
context.skip('#setCookie', () => {
context('#setCookie', () => {
beforeEach(() => {
cy.stub(Cypress.utils, 'addTwentyYears').returns(12345)
})
@@ -529,8 +872,7 @@ describe('src/cy/commands/cookies', () => {
})
describe('timeout', () => {
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
it.skip('sets timeout to Cypress.config(responseTimeout)', {
it('sets timeout to Cypress.config(responseTimeout)', {
responseTimeout: 2500,
}, () => {
Cypress.automation.resolves(null)
@@ -542,8 +884,7 @@ describe('src/cy/commands/cookies', () => {
})
})
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
it.skip('can override timeout', () => {
it('can override timeout', () => {
Cypress.automation.resolves(null)
const timeout = cy.spy(Promise.prototype, 'timeout')
@@ -553,8 +894,7 @@ describe('src/cy/commands/cookies', () => {
})
})
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
it.skip('clears the current timeout and restores after success', () => {
it('clears the current timeout and restores after success', () => {
Cypress.automation.resolves(null)
cy.timeout(100)
@@ -586,8 +926,7 @@ describe('src/cy/commands/cookies', () => {
return null
})
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
it.skip('logs once on error', function (done) {
it('logs once on error', function (done) {
const error = new Error('some err message')
error.name = 'foo'
@@ -607,8 +946,7 @@ describe('src/cy/commands/cookies', () => {
cy.setCookie('foo', 'bar')
})
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23444
it.skip('throws after timing out', function (done) {
it('throws after timing out', function (done) {
Cypress.automation.resolves(Promise.delay(1000))
cy.on('fail', (err) => {
@@ -697,6 +1035,20 @@ describe('src/cy/commands/cookies', () => {
cy.setCookie('foo', 'bar', { sameSite: 'None' })
})
it('when an invalid domain prop is supplied', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
expect(lastLog.get('error').message).to.eq('`cy.setCookie()` must be passed a valid domain name. You passed: `true`')
expect(lastLog.get('error').docsUrl).to.eq('https://on.cypress.io/setcookie')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.setCookie('foo', 'bar', { domain: true })
})
context('when setting an invalid cookie', () => {
it('throws an error if the backend responds with an error', (done) => {
const err = new Error('backend could not set cookie')
@@ -891,6 +1243,20 @@ describe('src/cy/commands/cookies', () => {
cy.clearCookie(123)
})
it('when an invalid domain prop is supplied', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
expect(lastLog.get('error').message).to.eq('`cy.clearCookie()` must be passed a valid domain name. You passed: `true`')
expect(lastLog.get('error').docsUrl).to.eq('https://on.cypress.io/clearcookie')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.clearCookie('foo', { domain: true })
})
})
describe('.log', () => {
@@ -1104,6 +1470,20 @@ describe('src/cy/commands/cookies', () => {
return null
})
it('when an invalid domain prop is supplied', function (done) {
cy.on('fail', (err) => {
const { lastLog } = this
expect(lastLog.get('error').message).to.eq('`cy.clearCookies()` must be passed a valid domain name. You passed: `true`')
expect(lastLog.get('error').docsUrl).to.eq('https://on.cypress.io/clearcookies')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.clearCookies({ domain: true })
})
it('logs once on \'get:cookies\' error', function (done) {
const error = new Error('some err message')

View File

@@ -691,6 +691,18 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
})
})
it('does not error when setting cookie with different domain', () => {
cy.get('[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://www.foobar.com:3500', () => {
cy.document().then((doc) => {
doc.cookie = 'key=value; domain=www.example.com'
})
// the cookie should not be set if the domain does not match
cy.document().its('cookie').should('equal', '')
})
})
it('works when setting cookie with extra, benign parts', () => {
cy.get('[data-cy="cross-origin-secondary-link"]').click()
cy.origin('http://www.foobar.com:3500', () => {

View File

@@ -79,28 +79,16 @@ function cookieValidatesSecurePrefix (options) {
return options.secure === false
}
interface InternalGetCookieOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
cookie?: Cypress.Cookie
}
interface InternalGetCookiesOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
cookies?: Cypress.Cookie[]
}
interface InternalSetCookieOptions extends Partial<Cypress.SetCookieOptions> {
_log?: Log
name: string
cookie?: Cypress.Cookie
}
type InternalClearCookieOptions = InternalGetCookieOptions
interface InternalClearCookiesOptions extends Partial<Cypress.Loggable & Cypress.Timeoutable> {
_log?: Log
cookies?: Cypress.Cookie[]
domain?: any
function validateDomainOption (domain: any, commandName: string, log: Log | undefined) {
if (domain !== undefined && domain !== null && !_.isString(domain)) {
$errUtils.throwErrByPath('cookies.invalid_domain', {
onFail: log,
args: {
cmd: commandName,
domain,
},
})
}
}
export default function (Commands, Cypress, cy, state, config) {
@@ -192,8 +180,8 @@ export default function (Commands, Cypress, cy, state, config) {
}
return Commands.addAll({
getCookie (name, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: InternalGetCookieOptions = _.defaults({}, userOptions, {
getCookie (name, userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -201,18 +189,18 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookie: Cypress.Cookie
let log: Log | undefined
if (options.log) {
options._log = Cypress.log({
log = Cypress.log({
message: name,
timeout: responseTimeout,
consoleProps () {
let c
const obj = {}
c = options.cookie
if (c) {
obj['Yielded'] = c
if (cookie) {
obj['Yielded'] = cookie
} else {
obj['Yielded'] = 'null'
obj['Note'] = `No cookie with the name: '${name}' was found.`
@@ -223,26 +211,24 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
const onFail = options._log
if (!_.isString(name)) {
$errUtils.throwErrByPath('getCookie.invalid_argument', { onFail })
$errUtils.throwErrByPath('getCookie.invalid_argument', { onFail: log })
}
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookie', { name }, options._log, responseTimeout)
.then(pickCookieProps)
.then((resp) => {
options.cookie = resp
validateDomainOption(options.domain, 'getCookie', log)
return resp
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookie', { name, domain: options.domain }, log, responseTimeout)
.then(pickCookieProps)
.tap((result) => {
cookie = result
})
.catch(handleBackendError('getCookie', 'reading the requested cookie from', onFail))
.catch(handleBackendError('getCookie', 'reading the requested cookie from', log))
}, options.timeout)
},
getCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: InternalGetCookiesOptions = _.defaults({}, userOptions, {
getCookies (userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -250,19 +236,19 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookies: Cypress.Cookie[] = []
let log: Log | undefined
if (options.log) {
options._log = Cypress.log({
log = Cypress.log({
message: '',
timeout: responseTimeout,
consoleProps () {
let c
const obj = {}
c = options.cookies
if (c) {
obj['Yielded'] = c
obj['Num Cookies'] = c.length
if (cookies.length) {
obj['Yielded'] = cookies
obj['Num Cookies'] = cookies.length
}
return obj
@@ -270,22 +256,20 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookies', _.pick(options, 'domain'), options._log, responseTimeout)
.then(pickCookieProps)
.then((resp) => {
options.cookies = resp
validateDomainOption(options.domain, 'getCookies', log)
return resp
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookies', _.pick(options, 'domain'), log, responseTimeout)
.then(pickCookieProps)
.tap((result: Cypress.Cookie[]) => {
cookies = result
})
.catch(handleBackendError('getCookies', 'reading cookies from', options._log))
.catch(handleBackendError('getCookies', 'reading cookies from', log))
}, options.timeout)
},
setCookie (name, value, userOptions: Partial<Cypress.SetCookieOptions> = {}) {
const options: InternalSetCookieOptions = _.defaults({}, userOptions, {
name,
value,
const options: Partial<Cypress.SetCookieOptions> = _.defaults({}, userOptions, {
path: '/',
secure: false,
httpOnly: false,
@@ -297,20 +281,19 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
const cookie = pickCookieProps(options)
const cookie = _.extend(pickCookieProps(options), { name, value })
let resultingCookie: Cypress.Cookie
let log: Log | undefined
if (options.log) {
options._log = Cypress.log({
log = Cypress.log({
message: [name, value],
timeout: responseTimeout,
consoleProps () {
let c
const obj = {}
c = options.cookie
if (c) {
obj['Yielded'] = c
if (resultingCookie) {
obj['Yielded'] = resultingCookie
}
return obj
@@ -318,13 +301,11 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
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,
onFail: log,
args: {
value: options.sameSite, // for clarity, throw the error with the user's unnormalized option
validValues: VALID_SAMESITE_VALUES,
@@ -336,7 +317,7 @@ export default function (Commands, Cypress, cy, state, config) {
// @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,
onFail: log,
args: {
value: options.sameSite, // for clarity, throw the error with the user's unnormalized option
},
@@ -344,32 +325,32 @@ export default function (Commands, Cypress, cy, state, config) {
}
if (!_.isString(name) || !_.isString(value)) {
$errUtils.throwErrByPath('setCookie.invalid_arguments', { onFail })
$errUtils.throwErrByPath('setCookie.invalid_arguments', { onFail: log })
}
if (options.name.startsWith('__Secure-') && cookieValidatesSecurePrefix(options)) {
$errUtils.throwErrByPath('setCookie.secure_prefix', { onFail })
if (name.startsWith('__Secure-') && cookieValidatesSecurePrefix(options)) {
$errUtils.throwErrByPath('setCookie.secure_prefix', { onFail: log })
}
if (options.name.startsWith('__Host-') && cookieValidatesHostPrefix(options)) {
$errUtils.throwErrByPath('setCookie.host_prefix', { onFail })
if (name.startsWith('__Host-') && cookieValidatesHostPrefix(options)) {
$errUtils.throwErrByPath('setCookie.host_prefix', { onFail: log })
}
validateDomainOption(options.domain, 'setCookie', log)
Cypress.emit('set:cookie', cookie)
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('set:cookie', cookie, options._log, responseTimeout)
return automateCookies('set:cookie', cookie, log, responseTimeout)
.then(pickCookieProps)
.then((resp) => {
options.cookie = resp
return resp
}).catch(handleBackendError('setCookie', 'setting the requested cookie in', onFail))
.tap((result) => {
resultingCookie = result
}).catch(handleBackendError('setCookie', 'setting the requested cookie in', log))
}, options.timeout)
},
clearCookie (name, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: InternalClearCookieOptions = _.defaults({}, userOptions, {
clearCookie (name, userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -377,20 +358,20 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookie: Cypress.Cookie
let log: Log | undefined
if (options.log) {
options._log = Cypress.log({
log = Cypress.log({
message: name,
timeout: responseTimeout,
consoleProps () {
let c
const obj = {}
obj['Yielded'] = 'null'
c = options.cookie
if (c) {
obj['Cleared Cookie'] = c
if (cookie) {
obj['Cleared Cookie'] = cookie
} else {
obj['Note'] = `No cookie with the name: '${name}' was found or removed.`
}
@@ -400,30 +381,30 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
const onFail = options._log
if (!_.isString(name)) {
$errUtils.throwErrByPath('clearCookie.invalid_argument', { onFail })
$errUtils.throwErrByPath('clearCookie.invalid_argument', { onFail: log })
}
validateDomainOption(options.domain, 'clearCookie', log)
Cypress.emit('clear:cookie', name)
// TODO: prevent clearing a cypress namespace
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('clear:cookie', { name }, options._log, responseTimeout)
return automateCookies('clear:cookie', { name, domain: options.domain }, log, responseTimeout)
.then(pickCookieProps)
.then((resp) => {
options.cookie = resp
.then((result) => {
cookie = result
// null out the current subject
return null
})
.catch(handleBackendError('clearCookie', 'clearing the requested cookie in', onFail))
.catch(handleBackendError('clearCookie', 'clearing the requested cookie in', log))
}, options.timeout)
},
clearCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: InternalClearCookiesOptions = _.defaults({}, userOptions, {
clearCookies (userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -431,19 +412,21 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookies: Cypress.Cookie[] = []
let log: Log | undefined
if (options.log) {
options._log = Cypress.log({
log = Cypress.log({
message: '',
timeout: responseTimeout,
consoleProps () {
const c = options.cookies
const obj = {}
obj['Yielded'] = 'null'
if (c && c.length) {
obj['Cleared Cookies'] = c
obj['Num Cookies'] = c.length
if (cookies.length) {
obj['Cleared Cookies'] = cookies
obj['Num Cookies'] = cookies.length
} else {
obj['Note'] = 'No cookies were found or removed.'
}
@@ -453,12 +436,14 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
validateDomainOption(options.domain, 'clearCookies', log)
Cypress.emit('clear:cookies')
return cy.retryIfCommandAUTOriginMismatch(() => {
return getAndClear(options._log, responseTimeout, { domain: options.domain })
.then((resp) => {
options.cookies = resp
return getAndClear(log, responseTimeout, { domain: options.domain })
.then((result) => {
cookies = result
// null out the current subject
return null
@@ -467,7 +452,7 @@ export default function (Commands, Cypress, cy, state, config) {
err.message = err.message.replace('getCookies', 'clearCookies')
throw err
})
.catch(handleBackendError('clearCookies', 'clearing cookies in', options._log))
.catch(handleBackendError('clearCookies', 'clearing cookies in', log))
}, options.timeout)
},
})

View File

@@ -325,6 +325,12 @@ export default {
docsUrl: 'https://on.cypress.io/session',
}
},
invalid_domain (obj) {
return {
message: `${cmd('{{cmd}}')} must be passed a valid domain name. You passed: \`{{domain}}\``,
docsUrl: `https://on.cypress.io/${_.toLower(obj.cmd)}`,
}
},
invalid_name (obj) {
return {
message: `${cmd('{{cmd}}')} must be passed an RFC-6265-compliant cookie name. You passed:\n\n\`{{name}}\``,

View File

@@ -63,6 +63,17 @@ export const patchDocumentCookie = (requestCookies: AutomationCookie[]) => {
})
}
const setCookie = (cookie: ToughCookie | string) => {
try {
return cookieJar.setCookie(cookie, url, undefined)
} catch (err) {
// it's possible setting the cookie fails because the domain does not
// match. this is expected and okay to do nothing, since it wouldn't be
// set in the browser anyway
return
}
}
// requestCookies are ones included with the page request that's now being
// injected into. they're captured by the proxy and included statically in
// the injection so they can be added here and available before page load
@@ -84,19 +95,21 @@ export const patchDocumentCookie = (requestCookies: AutomationCookie[]) => {
// value, but tough-cookie doesn't recognize it using an instanceof
// check and throws an error. because we can't, we have to massage
// some of the properties below to be correct
const cookie = cookieJar.setCookie(stringValue, url, undefined)!
const cookie = setCookie(stringValue)
cookie.sameSite = parsedCookie.sameSite
if (cookie) {
cookie.sameSite = parsedCookie.sameSite
if (!parsedCookie.path) {
cookie.path = '/'
if (!parsedCookie.path) {
cookie.path = '/'
}
// send the cookie to the server so it can be set in the browser via
// automation and in our server-side cookie jar so it's available
// to subsequent injections
sendCookieToServer(toughCookieToAutomationCookie(cookie, domain))
}
// send the cookie to the server so it can be set in the browser via
// automation and in our server-side cookie jar so it's available
// to subsequent injections
sendCookieToServer(toughCookieToAutomationCookie(cookie, domain))
return getDocumentCookieValue()
},
})
@@ -111,7 +124,7 @@ export const patchDocumentCookie = (requestCookies: AutomationCookie[]) => {
// the following listeners are called from Cypress cookie commands, so that
// the document.cookie value is updated optimistically
Cypress.on('set:cookie', (cookie: AutomationCookie) => {
cookieJar.setCookie(automationCookieToToughCookie(cookie, domain), url, undefined)
setCookie(automationCookieToToughCookie(cookie, domain))
})
Cypress.on('clear:cookie', (name: string) => {

View File

@@ -2,6 +2,7 @@
import _ from 'lodash'
import Bluebird from 'bluebird'
import type playwright from 'playwright-webkit'
import type { Protocol } from 'devtools-protocol'
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping'
import { cors, uri } from '@packages/network'
@@ -25,7 +26,7 @@ export type CyCookie = Pick<chrome.cookies.Cookie, 'name' | 'value' | 'expiratio
// Cypress uses the webextension-style filtering
// https://developer.chrome.com/extensions/cookies#method-getAll
type CyCookieFilter = chrome.cookies.GetAllDetails
export type CyCookieFilter = chrome.cookies.GetAllDetails
export function screencastOpts (everyNthFrame = Number(process.env.CYPRESS_EVERY_NTH_FRAME || 5)): Protocol.Page.StartScreencastRequest {
return {
@@ -61,7 +62,7 @@ export const _domainIsWithinSuperdomain = (domain: string, suffix: string) => {
return _.isEqual(suffixParts, domainParts.slice(domainParts.length - suffixParts.length))
}
export const _cookieMatches = (cookie: CyCookie, filter: CyCookieFilter) => {
export const cookieMatches = (cookie: CyCookie | playwright.Cookie, filter: CyCookieFilter) => {
if (filter.domain && !(cookie.domain && _domainIsWithinSuperdomain(cookie.domain, filter.domain))) {
return false
}
@@ -255,7 +256,7 @@ export class CdpAutomation {
.then((result: Protocol.Network.GetAllCookiesResponse) => {
return normalizeGetCookies(result.cookies)
.filter((cookie: CyCookie) => {
const matches = _cookieMatches(cookie, filter)
const matches = cookieMatches(cookie, filter)
debugVerbose('cookie matches filter? %o', { matches, cookie, filter })

View File

@@ -1,8 +1,7 @@
import _ from 'lodash'
import Debug from 'debug'
import type playwright from 'playwright-webkit'
import type { Automation } from '../automation'
import { normalizeResourceType } from './cdp_automation'
import { cookieMatches, normalizeResourceType, CyCookieFilter } from './cdp_automation'
import os from 'os'
import type { RunModeVideoApi } from '@packages/types'
import path from 'path'
@@ -15,11 +14,6 @@ export type CyCookie = Pick<chrome.cookies.Cookie, 'name' | 'value' | 'expiratio
sameSite?: 'no_restriction' | 'lax' | 'strict'
}
type CookieFilter = {
name: string
domain: string
}
const extensionMap = {
'no_restriction': 'None',
'lax': 'Lax',
@@ -65,29 +59,6 @@ const normalizeSetCookieProps = (cookie: CyCookie): playwright.Cookie => {
}
}
const _domainIsWithinSuperdomain = (domain: string, suffix: string) => {
const suffixParts = suffix.split('.').filter(_.identity)
const domainParts = domain.split('.').filter(_.identity)
return _.isEqual(suffixParts, domainParts.slice(domainParts.length - suffixParts.length))
}
const _cookieMatches = (cookie: any, filter: Record<string, any>) => {
if (filter.domain && !(cookie.domain && _domainIsWithinSuperdomain(cookie.domain, filter.domain))) {
return false
}
if (filter.path && filter.path !== cookie.path) {
return false
}
if (filter.name && filter.name !== cookie.name) {
return false
}
return true
}
let requestIdCounter = 1
const requestIdMap = new WeakMap<playwright.Request, string>()
let downloadIdCounter = 1
@@ -279,19 +250,23 @@ export class WebKitAutomation {
})
}
private async getCookies (): Promise<CyCookie[]> {
private async getCookies (filter: CyCookieFilter): Promise<CyCookie[]> {
const cookies = await this.context.cookies()
return cookies.map(normalizeGetCookieProps)
return cookies
.filter((cookie) => {
return cookieMatches(cookie, filter)
})
.map(normalizeGetCookieProps)
}
private async getCookie (filter: CookieFilter) {
private async getCookie (filter: CyCookieFilter) {
const cookies = await this.context.cookies()
if (!cookies.length) return null
const cookie = cookies.find((cookie) => {
return _cookieMatches(cookie, {
return cookieMatches(cookie, {
domain: filter.domain,
name: filter.name,
})
@@ -306,13 +281,16 @@ export class WebKitAutomation {
* @param filter the cookie to be cleared
* @returns the cleared cookie
*/
private async clearCookie (filter: CookieFilter): Promise<CookieFilter> {
private async clearCookie (filter: CyCookieFilter): Promise<CyCookieFilter> {
// webkit doesn't have a way to only clear certain cookies, so we have
// to clear all cookies and put back the ones we don't want cleared
const allCookies = await this.context.cookies()
const persistCookies = allCookies.filter((cookie) => {
return !_cookieMatches(cookie, filter)
return !cookieMatches(cookie, filter)
})
await this.context.clearCookies()
if (persistCookies.length) await this.context.addCookies(persistCookies)
return filter
@@ -322,12 +300,24 @@ export class WebKitAutomation {
* Clear all cookies
* @returns cookies cleared
*/
private async clearCookies (): Promise<CyCookie[]> {
const allCookies = await this.getCookies()
private async clearCookies (cookiesToClear: CyCookie[]): Promise<CyCookie[]> {
// webkit doesn't have a way to only clear certain cookies, so we have
// to clear all cookies and put back the ones we don't want cleared
const allCookies = await this.context.cookies()
const persistCookies = allCookies.filter((cookie) => {
return !cookiesToClear.find((cookieToClear) => {
return cookieMatches(cookie, cookieToClear)
})
})
debug('clear cookies: %o', cookiesToClear)
debug('put back cookies: %o', persistCookies)
await this.context.clearCookies()
return allCookies
if (persistCookies.length) await this.context.addCookies(persistCookies)
return cookiesToClear
}
private async takeScreenshot (data) {
@@ -347,7 +337,7 @@ export class WebKitAutomation {
case 'is:automation:client:connected':
return true
case 'get:cookies':
return await this.getCookies()
return await this.getCookies(data)
case 'get:cookie':
return await this.getCookie(data)
case 'set:cookie':
@@ -356,7 +346,7 @@ export class WebKitAutomation {
case 'set:cookies':
return await this.context.addCookies(data.map(normalizeSetCookieProps))
case 'clear:cookies':
return await this.clearCookies()
return await this.clearCookies(data)
case 'clear:cookie':
return await this.clearCookie(data)
case 'take:screenshot':

View File

@@ -2,7 +2,7 @@ const { expect, sinon } = require('../../spec_helper')
import {
CdpAutomation,
_cookieMatches,
cookieMatches,
_domainIsWithinSuperdomain,
CyCookie,
} from '../../../lib/browsers/cdp_automation'
@@ -47,7 +47,7 @@ context('lib/browsers/cdp_automation', () => {
})
})
context('._cookieMatches', () => {
context('.cookieMatches', () => {
it('matches as expected', () => {
[
{
@@ -61,7 +61,7 @@ context('lib/browsers/cdp_automation', () => {
expected: true,
},
].forEach(({ cookie, filter, expected }) => {
expect(_cookieMatches(cookie as CyCookie, filter)).to.eq(expected)
expect(cookieMatches(cookie as CyCookie, filter)).to.eq(expected)
})
})
})