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
4 changes: 4 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ _Released 01/27/2026 (PENDING)_
- Added a [`allowCypressEnv`](https://docs.cypress.io/app/references/configuration#Global) configuration option that disallows use of the deprecated `Cypress.env()` API. Addressed in [#33181](https://github.com/cypress-io/cypress/pull/33181).
- Introduced the new `Cypress.expose()` API, intended for use of public configuration of non-sensitive values. Addressed in [#33238](https://github.com/cypress-io/cypress/pull/33238).

**Bugfixes:**

- Fixed an issue where grouped commands inside `cy.origin()` did not reset the Command Log group level after completion. Fixed in [#33289](https://github.com/cypress-io/cypress/pull/33289).

**Misc:**

- The icon in the 'Open in IDE' button in the command log is now the correct size. Addresses [#32779](https://github.com/cypress-io/cypress/issues/32779). Addressed in [#33217](https://github.com/cypress-io/cypress/pull/33217).
Expand Down
32 changes: 32 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/commands/log.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ context('cy.origin log', { browser: '!webkit' }, () => {
})
})

it('resets group level after grouped commands in cy.origin', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.get('body').within(() => {
cy.get('#dom').should('exist')
})

cy.get('body').within(() => {
cy.get('#dom').should('exist')
})
})
.then(() => {
const withinLogs = logs.filter((log) => log.get('isCrossOriginLog') && log.get('name') === 'within')
const getBodyLogs = logs.filter((log) => {
return log.get('isCrossOriginLog')
&& log.get('name') === 'get'
&& log.get('message') === 'body'
})

expect(withinLogs, 'within logs').to.have.length(2)
expect(
withinLogs[1].get('groupLevel'),
'second within group level',
).to.eq(withinLogs[0].get('groupLevel'))

expect(getBodyLogs, 'get body logs').to.have.length(2)
expect(
getBodyLogs[1].get('groupLevel'),
'second get body group level',
).to.eq(getBodyLogs[0].get('groupLevel'))
})
})

it('primary origin does not override secondary origins timestamps', () => {
logs = []
const secondaryLogs = {
Expand Down
13 changes: 12 additions & 1 deletion packages/driver/src/cross-origin/origin_fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { syncConfigToCurrentOrigin, syncEnvToCurrentOrigin, syncExposeToCurrentO
import type { Runnable, Test } from 'mocha'
import { LogUtils } from '../cypress/log'

interface RunOriginFnState extends Cypress.ObjectLike {
originLogGroupLevel?: number
originLogGroupId?: string
}

interface RunOriginFnOptions {
config: Cypress.Config
args: any
Expand All @@ -14,7 +19,7 @@ interface RunOriginFnOptions {
file?: string
fn: string
skipConfigValidation: boolean
state: {}
state: RunOriginFnState
logCounter: number
}

Expand Down Expand Up @@ -149,6 +154,8 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
let queueFinished = false

reset(state)
cy.state('originLogGroupLevel', state?.originLogGroupLevel)
cy.state('originLogGroupId', state?.originLogGroupId)

// Set the counter for log ids
LogUtils.setCounter(logCounter)
Expand All @@ -166,6 +173,8 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
cy.state('onQueueEnd', () => {
queueFinished = true
setRunnableStateToPassed()
cy.state('originLogGroupLevel', undefined)
cy.state('originLogGroupId', undefined)
Cypress.specBridgeCommunicator.toPrimary('queue:finished', {
subject: cy.subject(),
}, {
Expand All @@ -175,6 +184,8 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {

cy.state('onFail', (err) => {
setRunnableStateToPassed()
cy.state('originLogGroupLevel', undefined)
cy.state('originLogGroupId', undefined)
if (queueFinished) {
// If the queue is already finished, send this event instead because
// the primary won't be listening for 'queue:finished' anymore
Expand Down
12 changes: 12 additions & 0 deletions packages/driver/src/cy/commands/origin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export default (Commands, Cypress: InternalCypress.Cypress, cy: Cypress.cy, stat
}

let log
let originLogGroupLevel
let originLogGroupId

logGroup(Cypress, {
name: 'origin',
Expand All @@ -71,6 +73,10 @@ export default (Commands, Cypress: InternalCypress.Cypress, cy: Cypress.cy, stat
// @ts-ignore TODO: revisit once log-grouping has more implementations
}, (_log) => {
log = _log
originLogGroupId = _log?.get('id')
originLogGroupLevel = (Cypress.state('logGroupIds') || []).length || 1
Cypress.state('originLogGroupId', originLogGroupId)
Cypress.state('originLogGroupLevel', originLogGroupLevel)
})

const validator = new Validator({
Expand Down Expand Up @@ -198,6 +204,10 @@ export default (Commands, Cypress: InternalCypress.Cypress, cy: Cypress.cy, stat
const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, $sourceMapUtils.getSourceMapProjectRoot())?.absoluteFile

try {
originLogGroupId = originLogGroupId ?? log?.get('id')
originLogGroupLevel = (Cypress.state('logGroupIds') || []).length || 1
Cypress.state('originLogGroupId', originLogGroupId)
Cypress.state('originLogGroupLevel', originLogGroupLevel)
// origin is a privileged command, meaning it has to be invoked
// from the spec or support file
await runPrivilegedCommand({
Expand Down Expand Up @@ -231,6 +241,8 @@ export default (Commands, Cypress: InternalCypress.Cypress, cy: Cypress.cy, stat
crossOriginCookies: Cypress.state('crossOriginCookies'),
isProtocolEnabled: Cypress.state('isProtocolEnabled'),
originUserInvocationStack: userInvocationStack,
originLogGroupLevel,
originLogGroupId,
},
config: preprocessConfig(Cypress.config()),
env: Cypress.config('allowCypressEnv') ? preprocessEnv(Cypress.env()) : undefined,
Expand Down
130 changes: 118 additions & 12 deletions packages/driver/src/cypress/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ const BLACKLIST_PROPS = 'snapshots'.split(' ')
const PROTOCOL_MESSAGE_TRUNCATION_LENGTH = 3000

let counter = 0
const LOG_ID_ORIGIN_RE = /^log-(.+)-\d+$/

const getLogOrigin = (id?: string) => {
if (!id) {
return window.location.origin
}

const match = LOG_ID_ORIGIN_RE.exec(id)

return match ? match[1] : window.location.origin
}

const getCurrentOriginLogGroupIds = (state: StateFunc) => {
const logGroupIds = state('logGroupIds') || []
const currentOrigin = window.location.origin
const effectiveLogGroupIds = logGroupIds.filter((id) => getLogOrigin(id) === currentOrigin)

if (effectiveLogGroupIds.length !== logGroupIds.length) {
state('logGroupIds', effectiveLogGroupIds)
}

return effectiveLogGroupIds
}

export const LogUtils = {
// mutate attrs by nulling out
Expand Down Expand Up @@ -222,19 +245,95 @@ const defaults = function (state: StateFunc, config, obj) {
},
})

const logGroupIds = state('logGroupIds') || []

if (logGroupIds.length) {
obj.group = _.last(logGroupIds)
obj.groupLevel = logGroupIds.length
if (Cypress.isCrossOriginSpecBridge) {
obj.originLogGroupLevel = state('originLogGroupLevel')
obj.originLogGroupId = state('originLogGroupId')
}

if (obj.groupEnd) {
state('logGroupIds', _.slice(logGroupIds, 0, -1))
const logGroupIds = getCurrentOriginLogGroupIds(state)
const logOrigin = getLogOrigin(obj.id)
const isDifferentOriginLog = logOrigin !== window.location.origin
const isCrossOriginId = typeof obj.id === 'string' && !obj.id.startsWith(`log-${window.location.origin}-`)
const isPrimaryReceivingCrossOriginLog = !Cypress.isCrossOriginSpecBridge
&& (obj.isCrossOriginLog || isDifferentOriginLog || isCrossOriginId)
let originLogGroupLevel = obj.originLogGroupLevel ?? state('originLogGroupLevel')
let originLogGroupId = obj.originLogGroupId ?? state('originLogGroupId')

if (!Cypress.isCrossOriginSpecBridge && (originLogGroupLevel == null || originLogGroupId == null)) {
const originMeta = (Cypress as any)?.primaryOriginCommunicator?.getOriginLogGroupMeta?.(logOrigin) || {}

if (originLogGroupLevel == null) {
originLogGroupLevel = originMeta.level
}

if (originLogGroupId == null) {
originLogGroupId = originMeta.id
}
}

if (obj.groupStart) {
state('logGroupIds', (logGroupIds).concat(obj.id))
const resolvedOriginLogGroupLevel = originLogGroupLevel ?? 0
const effectiveOriginLogGroupLevel = Cypress.isCrossOriginSpecBridge ? resolvedOriginLogGroupLevel : 0

if (isPrimaryReceivingCrossOriginLog) {
if (originLogGroupLevel != null) {
obj.originLogGroupLevel = originLogGroupLevel
}

if (originLogGroupId) {
obj.originLogGroupId = originLogGroupId
}

if (obj.groupLevel == null) {
if (originLogGroupLevel != null) {
obj.groupLevel = originLogGroupLevel
} else if (logGroupIds.length) {
obj.groupLevel = logGroupIds.length
}
}

if (!obj.group) {
if (originLogGroupId) {
obj.group = originLogGroupId
} else if (logGroupIds.length) {
obj.group = _.last(logGroupIds)
}
}

// cross-origin group start/end should not mutate primary log group state
if (obj.groupStart) {
obj.groupStart = false
}

if (obj.groupEnd) {
obj.groupEnd = false
}

// Never let cross-origin logs pollute primary log group state.
getCurrentOriginLogGroupIds(state)
} else {
if (logGroupIds.length) {
obj.group = _.last(logGroupIds)
obj.groupLevel = logGroupIds.length + effectiveOriginLogGroupLevel
} else if (effectiveOriginLogGroupLevel) {
obj.groupLevel = effectiveOriginLogGroupLevel

if (originLogGroupId) {
obj.group = originLogGroupId
}
}

const shouldTrackGroup = !isDifferentOriginLog && !isCrossOriginId
&& (!obj.isCrossOriginLog || Cypress.isCrossOriginSpecBridge)

if (shouldTrackGroup) {
if (obj.groupEnd) {
state('logGroupIds', _.slice(logGroupIds, 0, -1))
}

if (obj.groupStart) {
state('logGroupIds', (logGroupIds).concat(obj.id))
}
}
}

return obj
Expand Down Expand Up @@ -421,10 +520,11 @@ export class Log {
}

error (err) {
const logGroupIds = this.state('logGroupIds') || []
const logGroupIds = getCurrentOriginLogGroupIds(this.state)
const logOrigin = getLogOrigin(this.attributes.id)

// current log was responsible for creating the current log group so end the current group
if (_.last(logGroupIds) === this.attributes.id) {
if (logOrigin === window.location.origin && _.last(logGroupIds) === this.attributes.id) {
this.endGroup()
}

Expand Down Expand Up @@ -458,7 +558,13 @@ export class Log {
}

endGroup () {
this.state('logGroupIds', _.slice(this.state('logGroupIds'), 0, -1))
if (getLogOrigin(this.attributes.id) !== window.location.origin) {
return
}

const logGroupIds = getCurrentOriginLogGroupIds(this.state)

this.state('logGroupIds', _.slice(logGroupIds, 0, -1))
}

getError (err) {
Expand Down
2 changes: 2 additions & 0 deletions packages/driver/src/cypress/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface StateFunc {
(k: 'autLocation', v?: LocationObject): LocationObject
(k: 'originCommandBaseUrl', v?: string): string
(k: 'currentActiveOrigin', v?: string): string
(k: 'originLogGroupLevel', v?: number): number | undefined
(k: 'originLogGroupId', v?: string): string | undefined
(k: 'duringUserTestExecution', v?: boolean): boolean
(k: 'onQueueEnd', v?: () => void): () => void
(k: 'onFail', v?: (err: Error) => void): (err: Error) => void
Expand Down
4 changes: 4 additions & 0 deletions packages/driver/types/cypress/log.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ declare namespace Cypress {
functionName?: string
// whether or not to start a new log group
groupStart?: boolean
// cross-origin log group level offset from the primary origin
originLogGroupLevel?: number
// cross-origin log group id from the primary origin
originLogGroupId?: string
// whether or not the log should display in the reporter
hidden?: boolean
hookId?: number
Expand Down
Loading