Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2d8ce9c
chore: display self healed badge for cyPrompt
estrada9166 Oct 22, 2025
ee83ca0
Update with review
estrada9166 Oct 23, 2025
a254bfc
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 23, 2025
5c6e639
Revert changes
estrada9166 Oct 23, 2025
037d3aa
Merge branch 'alejandro/chore/implement-self-healed-badge' of github.…
estrada9166 Oct 23, 2025
6ca95bd
Cleanup code, add test
estrada9166 Oct 23, 2025
0c70478
Revert change
estrada9166 Oct 23, 2025
03e1515
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 23, 2025
a7e671f
Fix styles
estrada9166 Oct 23, 2025
11a4823
Merge branch 'alejandro/chore/implement-self-healed-badge' of github.…
estrada9166 Oct 23, 2025
e76a4b1
Revert change
estrada9166 Oct 23, 2025
81d81f6
Fix Cy test
estrada9166 Oct 23, 2025
4ddfc6e
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 23, 2025
f6215f4
Update with review
estrada9166 Oct 24, 2025
2fd7be5
Merge branch 'alejandro/chore/implement-self-healed-badge' of github.…
estrada9166 Oct 24, 2025
73e0b1d
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 24, 2025
33fa4ae
Fix Cy test
estrada9166 Oct 24, 2025
b276b0a
Merge branch 'alejandro/chore/implement-self-healed-badge' of github.…
estrada9166 Oct 24, 2025
6e1a261
Update with cursor review
estrada9166 Oct 24, 2025
e916838
Update with cursor review
estrada9166 Oct 24, 2025
ce2541a
Update with review
estrada9166 Oct 27, 2025
3bb6ee9
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 27, 2025
4a7ac25
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 27, 2025
2623a08
add feature changelog entry
jennifer-shehane Oct 27, 2025
554d505
Update date for release
jennifer-shehane Oct 27, 2025
1c0309d
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
jennifer-shehane Oct 27, 2025
2abb629
Update with review
estrada9166 Oct 28, 2025
cb5902a
Merge branch 'alejandro/chore/implement-self-healed-badge' of github.…
estrada9166 Oct 28, 2025
ecd2cb3
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 28, 2025
d76e131
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
jennifer-shehane Oct 29, 2025
56de58b
Merge branch 'develop' into alejandro/chore/implement-self-healed-badge
estrada9166 Oct 29, 2025
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
3 changes: 2 additions & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 15.6.0

_Released 10/20/2025 (PENDING)_
_Released 11/4/2025 (PENDING)_

**Features:**

- Added a 'Self-healed' badge to the Command Log when `cy.prompt()` steps automatically recover after the element they need is not found in the cache. Addressed in [#32802](https://github.com/cypress-io/cypress/pull/32802).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@estrada9166 You'll need to put a changelog entry with proper semver for every prompt feature since this is publicly released now.

- `cy.prompt()` will now show a warning in the `Get code` modal when there are unsaved changes in `Studio` that will be lost if the user saves the generated code. Addressed in [#32741](https://github.com/cypress-io/cypress/pull/32741).

**Bugfixes:**
Expand Down
146 changes: 146 additions & 0 deletions packages/reporter/cypress/e2e/commands.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1142,4 +1142,150 @@ describe('commands', { viewportHeight: 1000 }, () => {
cy.contains('GET /api/data/1').should('be.visible')
})
})

context('self-healed badge', () => {
it('renders the self-healed badge when the command is self-healed and move to top level when is closed', () => {
const nestedGroupId = addCommand(runner, {
name: 'session',
defaultCollapsedState: 'open',
state: 'passed',
type: 'child',
})

addCommand(runner, {
name: 'get',
message: 'do something',
state: 'passed',
groupLevel: 1,
group: nestedGroupId,
})

const nestedSessionGroupId = addCommand(runner, {
name: 'session',
defaultCollapsedState: 'open',
displayName: 'validate',
type: 'child',
groupLevel: 2,
group: nestedGroupId,
renderProps: {
selfHealed: true,
},
})

addCommand(runner, {
name: 'log',
message: 'inside of group',
state: 'passed',
group: nestedSessionGroupId,
})

cy.get('[data-cy="self-healed-badge-command"]').should('exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.percySnapshot('initial state')

cy.get('.command-message').eq(10).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
})

cy.get('.command-message').eq(12).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
})

cy.get('.command-expander').eq(1).click()

cy.percySnapshot('after clicking command expander')

cy.get('.command-message').eq(10).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
})

cy.get('.collapsible-header-inner').eq(2).click({ force: true })

cy.percySnapshot('after clicking collapsible header')

cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.get('.collapsible-header-inner').first().click()

cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.percySnapshot()
})

it('renders the self-healed badge when the command is self-healed and long text and move to top level when is closed', () => {
const nestedGroupId = addCommand(runner, {
name: 'session',
defaultCollapsedState: 'open',
state: 'passed',
type: 'child',
message: 'with long text to show wrapping works as expected and move to top level when is closed and is self-healed and is long text to show wrapping works as expected',
})

addCommand(runner, {
name: 'get',
message: 'do something',
state: 'passed',
groupLevel: 1,
group: nestedGroupId,
})

const nestedSessionGroupId = addCommand(runner, {
name: 'session',
defaultCollapsedState: 'open',
displayName: 'validate',
type: 'child',
groupLevel: 2,
group: nestedGroupId,
message: 'with long text to show wrapping works as expected and move to top level when is closed and is self-healed and is long text to show wrapping works as expected',
renderProps: {
selfHealed: true,
},
})

addCommand(runner, {
name: 'log',
message: 'inside of group',
state: 'passed',
group: nestedSessionGroupId,
})

cy.get('[data-cy="self-healed-badge-command"]').should('exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.percySnapshot('initial state')

cy.get('.command-message').eq(10).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
})

cy.get('.command-message').eq(12).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
})

cy.get('.command-expander').eq(1).click()

cy.percySnapshot('after clicking command expander')

cy.get('.command-message').eq(10).within(() => {
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
})

cy.get('.collapsible-header-inner').eq(2).click({ force: true })

cy.percySnapshot('after clicking collapsible header')

cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.get('.collapsible-header-inner').first().click()

cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
cy.get('[data-cy="self-healed-badge-test"]').should('exist')

cy.percySnapshot()
})
})
})
36 changes: 36 additions & 0 deletions packages/reporter/cypress/e2e/unit/test_model.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,40 @@ describe('Test model', () => {
expect(test.isOpen).eq(true)
})
})

context('#isSelfHealed', () => {
it('false by default', () => {
const test = createTest()

expect(test.isSelfHealed).to.be.false
})

it('true when there is a self-healed command', () => {
const test = createTest()

test.addLog(createCommand({ renderProps: { selfHealed: true } }))
expect(test.isSelfHealed).to.be.true
})

it('true when there is a self-healed command in an attempt', () => {
const test = createTest({
currentRetry: 2,
prevAttempts: [
{ id: 'r3', currentRetry: 0, state: 'failed', hooks: [] },
{ id: 'r3', currentRetry: 1, state: 'failed', hooks: [] },
],
})

// Add a regular command to the first attempt
test.addLog(createCommand({ testCurrentRetry: 0, renderProps: { selfHealed: false } }))

// Add a self-healed command to the second attempt
test.addLog(createCommand({ testCurrentRetry: 1, renderProps: { selfHealed: true } }))

// Add a regular command to the current (third) attempt
test.addLog(createCommand({ testCurrentRetry: 2, renderProps: { selfHealed: false } }))

expect(test.isSelfHealed).to.be.true
})
})
})
6 changes: 6 additions & 0 deletions packages/reporter/src/commands/command-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface RenderProps {
}>
status?: InterceptStatuses | XHRStatuses
wentToOrigin?: boolean
selfHealed?: boolean
}

export interface CommandProps extends InstrumentProps {
Expand Down Expand Up @@ -163,6 +164,7 @@ export default class Command extends Instrument {
hasChildren: computed,
showError: computed,
setGroup: action,
isSelfHealed: computed,
})

if (props.err) {
Expand Down Expand Up @@ -278,4 +280,8 @@ export default class Command extends Instrument {
_isPending () {
return this.state === 'pending'
}

get isSelfHealed () {
return (!!this.renderProps.selfHealed || (this.hasChildren && !this.isOpen && this.children.some((child) => child.isSelfHealed)))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: MobX Computed Getter Not Registered

The isSelfHealed getter is defined but not registered in the makeObservable call in the constructor (lines 150-171). This getter depends on observable/computed properties (renderProps, hasChildren, isOpen, children) but without being declared as computed in makeObservable, MobX won't properly track it as a reactive computed value. This can cause React components that depend on model.isSelfHealed to fail to re-render when the underlying dependencies change. The getter should be added to the makeObservable configuration: isSelfHealed: computed.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Self-Heal Status Not Propagating Through Nested Groups

The isSelfHealed getter checks child.renderProps.selfHealed directly when a group is closed, but it should check child.isSelfHealed instead. This prevents proper propagation of the self-healed status through nested collapsed groups. When a child command's isSelfHealed computed property returns true (because one of its descendants is self-healed and the child is closed), the parent won't detect it because the code only checks the direct renderProps.selfHealed property, not the computed value.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
4 changes: 4 additions & 0 deletions packages/reporter/src/commands/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import HiddenIcon from '@packages/frontend-shared/src/assets/icons/general-eye-c
import PinIcon from '@packages/frontend-shared/src/assets/icons/object-pin_x16.svg'
import RunningIcon from '@packages/frontend-shared/src/assets/icons/status-running_x16.svg'
import { IconTechnologyAngleBrackets } from '@cypress-design/react-icon'
import { SelfHealedBadge } from '../lib/selfHealedBadge'

const displayName = (model: CommandModel) => model.displayName || model.name
const nameClassName = (name: string) => name.replace(/(\s+)/g, '-')
Expand Down Expand Up @@ -279,6 +280,9 @@ const Message: React.FC<MessageProps> = observer(({ model }: MessageProps) => (
className='command-message-text'
dangerouslySetInnerHTML={{ __html: formattedMessage(model.displayMessage, model.name) }}
/>}
{model.isSelfHealed && (
<SelfHealedBadge source='command' />
)}
</span>
))

Expand Down
1 change: 1 addition & 0 deletions packages/reporter/src/commands/commands.scss
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@
white-space: initial;
word-wrap: inherit;
width: 100%;
margin-right: 8px;
}

// Styles for Uncaught Exception
Expand Down
33 changes: 33 additions & 0 deletions packages/reporter/src/lib/selfHealedBadge.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.command-self-healed-badge {
background-color: $gray-1000;
border-radius: 4px;
display: inline-flex;
height: 20px;
border: 1px solid $gray-900;
gap: 4px;
padding: 0 4px;
color: $jade-300;
font-family: $font-system;
font-size: 14px;
font-weight: 400;
margin-right: 4px;
align-items: center;
justify-content: center;
white-space: nowrap;
text-transform: none;
vertical-align: middle;

.command-info:hover & {
border-color: $gray-800;
}

}

.command-self-healed-badge-command {
height: 16px;
font-size: 12px;
}

.command-self-healed-badge-test {
margin-left: 8px;
}
14 changes: 14 additions & 0 deletions packages/reporter/src/lib/selfHealedBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import { IconGeneralSparkleSingleSmall } from '@cypress-design/react-icon'
import cs from 'classnames'

export const SelfHealedBadge = ({ source }: { source: 'command' | 'test' }) => {
return (
<div className={cs('command-self-healed-badge', { 'command-self-healed-badge-command': source === 'command', 'command-self-healed-badge-test': source === 'test' })} data-cy={`self-healed-badge-${source}`}>
<IconGeneralSparkleSingleSmall strokeColor='jade-300' fillColor='gray-1000' />
<span>
Self-healed
</span>
</div>
)
}
1 change: 1 addition & 0 deletions packages/reporter/src/main-runner.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@import 'lib/switch';
@import 'lib/tag';
@import 'lib/tooltip';
@import 'lib/selfHealedBadge';
@import '@reach/dialog/styles.css';
// import all other scss files in src except if they are in lib
// or their file name is `selector-playground` or `main`
Expand Down
1 change: 1 addition & 0 deletions packages/reporter/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@import 'lib/switch';
@import 'lib/tag';
@import 'lib/tooltip';
@import 'lib/selfHealedBadge';
@import '@reach/dialog/styles.css';
// import all other scss files in src except if they are in lib
// or their file name is `selector-playground` or `main`
Expand Down
9 changes: 9 additions & 0 deletions packages/reporter/src/test/test-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default class Test extends Runnable {
hasRetried: computed,
isActive: computed,
currentRetry: computed,
isSelfHealed: computed,
start: action,
update: action,
setIsOpen: action,
Expand Down Expand Up @@ -257,4 +258,12 @@ export default class Test extends Runnable {

return null
}

get isSelfHealed () {
// Compute self-healed status from the commands in all attempts
// This ensures the badge is shown correctly even across retries
return _.some(this.attempts, (attempt: Attempt) => {
return _.some(attempt.commands, (command) => command.isSelfHealed)
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Self-Heal Flag Persistence Across Test Attempts

The _isSelfHealed flag is never reset between test retries. Once set to true when a self-healed command is encountered in one attempt, it remains true for all subsequent attempts, even if those attempts don't contain self-healed commands. The isSelfHealed getter should compute the status from the current attempt(s) rather than storing a persistent flag. This will cause the self-healed badge to incorrectly display on tests that had self-healed commands in earlier attempts but not in the final attempt.

Fix in Cursor Fix in Web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@estrada9166 Can you test this?

}
4 changes: 4 additions & 0 deletions packages/reporter/src/test/test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Attempts from '../attempts/attempts'
import StateIcon from '../lib/state-icon'
import { LaunchStudioIcon } from '../components/LaunchStudioIcon'
import { useScrollIntoView } from '../lib/useScrollIntoView'
import { SelfHealedBadge } from '../lib/selfHealedBadge'

interface TestProps {
events?: Events
Expand Down Expand Up @@ -45,6 +46,9 @@ const Test: React.FC<TestProps> = observer(({ model, events: eventsProps = event
<span className='runnable-title'>
<span>{model.title}</span>
<span className='visually-hidden'>{model.state}</span>
{model.isSelfHealed && (
<SelfHealedBadge source='test' />
)}
</span>
{_controls()}
</>)
Expand Down
Loading