Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- The default `keystrokeDelay` for `cy.type()` has been changed from `10` to `0` to improve performance of typing-heavy test runs. Users who relied on the implicit 10ms delay can restore it by setting `keystrokeDelay: 10` in their Cypress config or on `Cypress.Keyboard`. Addresses [#33523](https://github.com/cypress-io/cypress/issues/33523).
- Rewrote the experimental `experimentalFastVisibility` algorithm to delegate to the browser's native `Element.checkVisibility()` with a zero-dimension guard. Addressed in [#33781](https://github.com/cypress-io/cypress/pull/33781).
- The `experimentalFastVisibility` boolean flag has been replaced with a new `visibilityStrategy` config option that accepts `'legacy'` or `'modern'`, defaulting to `'modern'`. The modern visibility algorithm (based on `Element.checkVisibility()`) is now the default for all users. The option is deprecated — set `visibilityStrategy: 'legacy'` only if you need the old algorithm when migrating to the modern visibility algorithm. Addressed in [#33794](https://github.com/cypress-io/cypress/pull/33794).
- [`cy.getCookie()`](https://on.cypress.io/getcookie) and [`cy.getCookies()`](https://on.cypress.io/getcookies) are now query commands. They re-read the cookie(s) from the browser and retry any attached assertions until they pass or the command times out, so `cy.getCookie('token').should('exist')` now waits for a cookie that is set asynchronously (for example, after a login request resolves) instead of failing on the first read. As a result, their timeout now follows `defaultCommandTimeout` (default `4000`) rather than `responseTimeout` (default `30000`), and to overwrite these commands you must now use [`Cypress.Commands.overwriteQuery()`](https://on.cypress.io/api/cypress-api/custom-queries) instead of `Cypress.Commands.overwrite()`. Addresses [#4802](https://github.com/cypress-io/cypress/issues/4802).
- [`cy.getCookie()`](https://on.cypress.io/getcookie), [`cy.getCookies()`](https://on.cypress.io/getcookies) and [`cy.getAllCookies()`](https://on.cypress.io/getallcookies) are now query commands. They re-read the cookie(s) from the browser and retry any attached assertions until they pass or the command times out, so `cy.getCookie('token').should('exist')` now waits for a cookie that is set asynchronously (for example, after a login request resolves) instead of failing on the first read. As a result, their timeout now follows `defaultCommandTimeout` (default `4000`) rather than `responseTimeout` (default `30000`), and to overwrite these commands you must now use [`Cypress.Commands.overwriteQuery()`](https://on.cypress.io/api/cypress-api/custom-queries) instead of `Cypress.Commands.overwrite()`. Addresses [#4802](https://github.com/cypress-io/cypress/issues/4802).
- Removed the `experimentalSourceRewriting` configuration option. The experimental AST-based source rewriting was removed in favor of the default regex-based source rewriting, so default behavior is unchanged. You can safely remove this option from your config. Addresses [#34213](https://github.com/cypress-io/cypress/issues/34213).

**Deprecations:**
Expand Down
26 changes: 13 additions & 13 deletions packages/driver/cypress/e2e/commands/cookies.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ describe('src/cy/commands/cookies', () => {
})

describe('timeout', () => {
it('sets timeout to Cypress.config(responseTimeout)', { responseTimeout: 2500 }, () => {
it('sets timeout to Cypress.config(defaultCommandTimeout)', { defaultCommandTimeout: 2500 }, () => {
Cypress.automation.resolves([])

const timeout = cy.spy(Promise.prototype, 'timeout')
Expand All @@ -899,20 +899,20 @@ describe('src/cy/commands/cookies', () => {
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')
// https://github.com/cypress-io/cypress/issues/4802
it('retries reading cookies until an assertion passes', () => {
const get = Cypress.automation.withArgs('get:cookies')

cy.getAllCookies().then(() => {
expect(cy.clearTimeout).to.be.calledWith('get:cookies')
get.resolves([])
get.onCall(2).resolves([
{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: false },
])

// restores the timeout afterwards
expect(cy.timeout()).to.eq(100)
})
cy.getAllCookies().should('have.length', 1).then((cookies) => {
expect(cookies[0].name).to.eq('foo')
expect(get.callCount).to.be.gte(3)
})
})

Expand Down Expand Up @@ -962,7 +962,7 @@ describe('src/cy/commands/cookies', () => {
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.message).to.eq('Timed out retrying after 50ms: `cy.getAllCookies()` timed out waiting `50ms` to complete.')
expect(err.docsUrl).to.eq('https://on.cypress.io/getallcookies')

done()
Expand Down
134 changes: 66 additions & 68 deletions packages/driver/src/cy/commands/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
}
}

// getCookie and getCookies are query commands: they re-pull the cookie(s)
// from the browser and re-run any attached assertions until they pass or the
// command times out. This allows `cy.getCookie('foo').should('exist')` to wait
// for a cookie that is set asynchronously (e.g. after a login request resolves).
// getCookie, getCookies and getAllCookies are query commands: they re-pull
// the cookie(s) from the browser and re-run any attached assertions until
// they pass or the command times out. This allows `cy.getCookie('foo').should('exist')`
// to wait for a cookie that is set asynchronously (e.g. after a login request resolves).
// @see https://github.com/cypress-io/cypress/issues/4802
type CookieQuery = 'get:cookie' | 'get:cookies'

interface CookieQueryParams {
commandName: 'getCookie' | 'getCookies'
commandName: 'getCookie' | 'getCookies' | 'getAllCookies'
event: CookieQuery
action: string
buildOptions: () => AutomationEventsAndOptions[CookieQuery]
Expand All @@ -217,12 +217,16 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
// long a single automation round-trip may take. defaults to
// `defaultCommandTimeout`, consistent with other query commands.
timeout: number
// getAllCookies reads cookies across all domains without touching the
// AUT's location, so it opts out of the AUT communication check and keeps
// working when the AUT is cross-origin.
checkAUTCommunication?: boolean
}

// shared retry/automation plumbing for the getCookie(s) query commands.
// returns a `fetch` function that (re-)reads cookies in the background and a
// `getSubject` function suitable for returning from a query command.
function createCookieQuery ({ commandName, event, action, buildOptions, onResult, log, timeout }: CookieQueryParams) {
function createCookieQuery ({ commandName, event, action, buildOptions, onResult, log, timeout, checkAUTCommunication = true }: CookieQueryParams) {
let hasResult = false
let result: any
let pending: Promise<void> | null = null
Expand All @@ -245,8 +249,11 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
// mismatch here is retryable - keep retrying until the AUT is reachable
// or the command times out (matching the previous
// retryIfCommandAUTOriginMismatch behavior).
// @ts-expect-error
Cypress.ensure.commandCanCommunicateWithAUT(cy)
if (checkAUTCommunication) {
// @ts-expect-error
Cypress.ensure.commandCanCommunicateWithAUT(cy)
}

automationOptions = buildOptions()
} catch (err: any) {
mostRecentError = err
Expand Down Expand Up @@ -320,6 +327,40 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
return getSubject
}

function setupCookiesQuery (userOptions: Cypress.CookieOptions, message: string | { domain: string }) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})

const timeout = options.timeout || config('defaultCommandTimeout')

let cookies: Cypress.Cookie[] = []
const log: Cypress.Log | undefined = Cypress.log({
message,
hidden: !options.log,
timeout,
consoleProps () {
const obj = {}

if (cookies.length) {
obj['Yielded'] = cookies
obj['Num Cookies'] = cookies.length
}

return obj
},
})

return {
options,
timeout,
log,
onResult: (result: Cypress.Cookie[]) => {
cookies = result
},
}
}

Commands.addQuery('getCookie', function getCookie (name: string, userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
Expand Down Expand Up @@ -370,31 +411,10 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
})

Commands.addQuery('getCookies', function getCookies (userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})

const timeout = options.timeout || config('defaultCommandTimeout')
const { options, timeout, log, onResult } = setupCookiesQuery(userOptions, userOptions.domain ? { domain: userOptions.domain } : '')

this.set('timeout', timeout)

let cookies: Cypress.Cookie[] = []
const log: Cypress.Log | undefined = Cypress.log({
message: userOptions.domain ? { domain: userOptions.domain } : '',
hidden: !options.log,
timeout,
consoleProps () {
const obj = {}

if (cookies.length) {
obj['Yielded'] = cookies
obj['Num Cookies'] = cookies.length
}

return obj
},
})

validateDomainOption(userOptions.domain, 'getCookies', log)

return createCookieQuery({
Expand All @@ -404,52 +424,30 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
// getDefaultDomain() is called here (rather than above where default
// options are set) so a cross-origin access error is retried.
buildOptions: () => ({ domain: options.domain || getDefaultDomain() }),
onResult: (result: Cypress.Cookie[]) => {
cookies = result
},
onResult,
log,
timeout,
})
})

return Commands.addAll({
getAllCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
timeout: config('responseTimeout'),
})

let cookies: Cypress.Cookie[] = []
const log: Cypress.Log | undefined = Cypress.log({
message: '',
hidden: !options.log,
timeout: options.timeout,
consoleProps () {
const obj = {}
Commands.addQuery('getAllCookies', function getAllCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
Comment thread
jennifer-shehane marked this conversation as resolved.
const { timeout, log, onResult } = setupCookiesQuery(userOptions, '')

if (cookies.length) {
obj['Yielded'] = cookies
obj['Num Cookies'] = cookies.length
}

return obj
},
})
this.set('timeout', timeout)

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))
},
return createCookieQuery({
commandName: 'getAllCookies',
event: 'get:cookies',
action: 'reading cookies from',
buildOptions: () => ({}),
onResult,
log,
timeout,
checkAUTCommunication: false,
})
})

return Commands.addAll({
setCookie (name: string, value: string, userOptions: Partial<Cypress.SetCookieOptions> = {}) {
const options: Partial<Cypress.SetCookieOptions> = _.defaults({}, userOptions, {
path: '/',
Expand Down
Loading