diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index ddf72e132e3..fd6a629d323 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -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:** diff --git a/packages/driver/cypress/e2e/commands/cookies.cy.js b/packages/driver/cypress/e2e/commands/cookies.cy.js index b12c9412b0c..9f13f4c947d 100644 --- a/packages/driver/cypress/e2e/commands/cookies.cy.js +++ b/packages/driver/cypress/e2e/commands/cookies.cy.js @@ -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') @@ -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) }) }) @@ -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() diff --git a/packages/driver/src/cy/commands/cookies.ts b/packages/driver/src/cy/commands/cookies.ts index 1f658b8e240..7699ee7c85b 100644 --- a/packages/driver/src/cy/commands/cookies.ts +++ b/packages/driver/src/cy/commands/cookies.ts @@ -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] @@ -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 | null = null @@ -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 @@ -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, @@ -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({ @@ -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 = {}) { - 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 = {}) { + 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 = {}) { const options: Partial = _.defaults({}, userOptions, { path: '/',