Skip to content

Commit b92dcdb

Browse files
feat: display self healed badge for cy.prompt (#32802)
* chore: display self healed badge for cyPrompt * Update with review * Revert changes * Cleanup code, add test * Revert change * Fix styles * Revert change * Fix Cy test * Update with review * Fix Cy test * Update with cursor review * Update with cursor review * Update with review * add feature changelog entry * Update date for release * Update with review --------- Co-authored-by: Jennifer Shehane <[email protected]> Co-authored-by: Jennifer Shehane <[email protected]>
1 parent 5988dc3 commit b92dcdb

File tree

12 files changed

+257
-1
lines changed

12 files changed

+257
-1
lines changed

cli/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
22
## 15.6.0
33

4-
_Released 10/20/2025 (PENDING)_
4+
_Released 11/4/2025 (PENDING)_
55

66
**Features:**
77

8+
- 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).
89
- `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).
910

1011
**Bugfixes:**

packages/reporter/cypress/e2e/commands.cy.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,4 +1142,150 @@ describe('commands', { viewportHeight: 1000 }, () => {
11421142
cy.contains('GET /api/data/1').should('be.visible')
11431143
})
11441144
})
1145+
1146+
context('self-healed badge', () => {
1147+
it('renders the self-healed badge when the command is self-healed and move to top level when is closed', () => {
1148+
const nestedGroupId = addCommand(runner, {
1149+
name: 'session',
1150+
defaultCollapsedState: 'open',
1151+
state: 'passed',
1152+
type: 'child',
1153+
})
1154+
1155+
addCommand(runner, {
1156+
name: 'get',
1157+
message: 'do something',
1158+
state: 'passed',
1159+
groupLevel: 1,
1160+
group: nestedGroupId,
1161+
})
1162+
1163+
const nestedSessionGroupId = addCommand(runner, {
1164+
name: 'session',
1165+
defaultCollapsedState: 'open',
1166+
displayName: 'validate',
1167+
type: 'child',
1168+
groupLevel: 2,
1169+
group: nestedGroupId,
1170+
renderProps: {
1171+
selfHealed: true,
1172+
},
1173+
})
1174+
1175+
addCommand(runner, {
1176+
name: 'log',
1177+
message: 'inside of group',
1178+
state: 'passed',
1179+
group: nestedSessionGroupId,
1180+
})
1181+
1182+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1183+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1184+
1185+
cy.percySnapshot('initial state')
1186+
1187+
cy.get('.command-message').eq(10).within(() => {
1188+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1189+
})
1190+
1191+
cy.get('.command-message').eq(12).within(() => {
1192+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1193+
})
1194+
1195+
cy.get('.command-expander').eq(1).click()
1196+
1197+
cy.percySnapshot('after clicking command expander')
1198+
1199+
cy.get('.command-message').eq(10).within(() => {
1200+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1201+
})
1202+
1203+
cy.get('.collapsible-header-inner').eq(2).click({ force: true })
1204+
1205+
cy.percySnapshot('after clicking collapsible header')
1206+
1207+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1208+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1209+
1210+
cy.get('.collapsible-header-inner').first().click()
1211+
1212+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1213+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1214+
1215+
cy.percySnapshot()
1216+
})
1217+
1218+
it('renders the self-healed badge when the command is self-healed and long text and move to top level when is closed', () => {
1219+
const nestedGroupId = addCommand(runner, {
1220+
name: 'session',
1221+
defaultCollapsedState: 'open',
1222+
state: 'passed',
1223+
type: 'child',
1224+
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',
1225+
})
1226+
1227+
addCommand(runner, {
1228+
name: 'get',
1229+
message: 'do something',
1230+
state: 'passed',
1231+
groupLevel: 1,
1232+
group: nestedGroupId,
1233+
})
1234+
1235+
const nestedSessionGroupId = addCommand(runner, {
1236+
name: 'session',
1237+
defaultCollapsedState: 'open',
1238+
displayName: 'validate',
1239+
type: 'child',
1240+
groupLevel: 2,
1241+
group: nestedGroupId,
1242+
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',
1243+
renderProps: {
1244+
selfHealed: true,
1245+
},
1246+
})
1247+
1248+
addCommand(runner, {
1249+
name: 'log',
1250+
message: 'inside of group',
1251+
state: 'passed',
1252+
group: nestedSessionGroupId,
1253+
})
1254+
1255+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1256+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1257+
1258+
cy.percySnapshot('initial state')
1259+
1260+
cy.get('.command-message').eq(10).within(() => {
1261+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1262+
})
1263+
1264+
cy.get('.command-message').eq(12).within(() => {
1265+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1266+
})
1267+
1268+
cy.get('.command-expander').eq(1).click()
1269+
1270+
cy.percySnapshot('after clicking command expander')
1271+
1272+
cy.get('.command-message').eq(10).within(() => {
1273+
cy.get('[data-cy="self-healed-badge-command"]').should('exist')
1274+
})
1275+
1276+
cy.get('.collapsible-header-inner').eq(2).click({ force: true })
1277+
1278+
cy.percySnapshot('after clicking collapsible header')
1279+
1280+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1281+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1282+
1283+
cy.get('.collapsible-header-inner').first().click()
1284+
1285+
cy.get('[data-cy="self-healed-badge-command"]').should('not.exist')
1286+
cy.get('[data-cy="self-healed-badge-test"]').should('exist')
1287+
1288+
cy.percySnapshot()
1289+
})
1290+
})
11451291
})

packages/reporter/cypress/e2e/unit/test_model.cy.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,40 @@ describe('Test model', () => {
368368
expect(test.isOpen).eq(true)
369369
})
370370
})
371+
372+
context('#isSelfHealed', () => {
373+
it('false by default', () => {
374+
const test = createTest()
375+
376+
expect(test.isSelfHealed).to.be.false
377+
})
378+
379+
it('true when there is a self-healed command', () => {
380+
const test = createTest()
381+
382+
test.addLog(createCommand({ renderProps: { selfHealed: true } }))
383+
expect(test.isSelfHealed).to.be.true
384+
})
385+
386+
it('true when there is a self-healed command in an attempt', () => {
387+
const test = createTest({
388+
currentRetry: 2,
389+
prevAttempts: [
390+
{ id: 'r3', currentRetry: 0, state: 'failed', hooks: [] },
391+
{ id: 'r3', currentRetry: 1, state: 'failed', hooks: [] },
392+
],
393+
})
394+
395+
// Add a regular command to the first attempt
396+
test.addLog(createCommand({ testCurrentRetry: 0, renderProps: { selfHealed: false } }))
397+
398+
// Add a self-healed command to the second attempt
399+
test.addLog(createCommand({ testCurrentRetry: 1, renderProps: { selfHealed: true } }))
400+
401+
// Add a regular command to the current (third) attempt
402+
test.addLog(createCommand({ testCurrentRetry: 2, renderProps: { selfHealed: false } }))
403+
404+
expect(test.isSelfHealed).to.be.true
405+
})
406+
})
371407
})

packages/reporter/src/commands/command-model.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface RenderProps {
2121
}>
2222
status?: InterceptStatuses | XHRStatuses
2323
wentToOrigin?: boolean
24+
selfHealed?: boolean
2425
}
2526

2627
export interface CommandProps extends InstrumentProps {
@@ -163,6 +164,7 @@ export default class Command extends Instrument {
163164
hasChildren: computed,
164165
showError: computed,
165166
setGroup: action,
167+
isSelfHealed: computed,
166168
})
167169

168170
if (props.err) {
@@ -278,4 +280,8 @@ export default class Command extends Instrument {
278280
_isPending () {
279281
return this.state === 'pending'
280282
}
283+
284+
get isSelfHealed () {
285+
return (!!this.renderProps.selfHealed || (this.hasChildren && !this.isOpen && this.children.some((child) => child.isSelfHealed)))
286+
}
281287
}

packages/reporter/src/commands/command.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import HiddenIcon from '@packages/frontend-shared/src/assets/icons/general-eye-c
2525
import PinIcon from '@packages/frontend-shared/src/assets/icons/object-pin_x16.svg'
2626
import RunningIcon from '@packages/frontend-shared/src/assets/icons/status-running_x16.svg'
2727
import { IconTechnologyAngleBrackets } from '@cypress-design/react-icon'
28+
import { SelfHealedBadge } from '../lib/selfHealedBadge'
2829

2930
const displayName = (model: CommandModel) => model.displayName || model.name
3031
const nameClassName = (name: string) => name.replace(/(\s+)/g, '-')
@@ -279,6 +280,9 @@ const Message: React.FC<MessageProps> = observer(({ model }: MessageProps) => (
279280
className='command-message-text'
280281
dangerouslySetInnerHTML={{ __html: formattedMessage(model.displayMessage, model.name) }}
281282
/>}
283+
{model.isSelfHealed && (
284+
<SelfHealedBadge source='command' />
285+
)}
282286
</span>
283287
))
284288

packages/reporter/src/commands/commands.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@
421421
white-space: initial;
422422
word-wrap: inherit;
423423
width: 100%;
424+
margin-right: 8px;
424425
}
425426

426427
// Styles for Uncaught Exception
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.command-self-healed-badge {
2+
background-color: $gray-1000;
3+
border-radius: 4px;
4+
display: inline-flex;
5+
height: 20px;
6+
border: 1px solid $gray-900;
7+
gap: 4px;
8+
padding: 0 4px;
9+
color: $jade-300;
10+
font-family: $font-system;
11+
font-size: 14px;
12+
font-weight: 400;
13+
margin-right: 4px;
14+
align-items: center;
15+
justify-content: center;
16+
white-space: nowrap;
17+
text-transform: none;
18+
vertical-align: middle;
19+
20+
.command-info:hover & {
21+
border-color: $gray-800;
22+
}
23+
24+
}
25+
26+
.command-self-healed-badge-command {
27+
height: 16px;
28+
font-size: 12px;
29+
}
30+
31+
.command-self-healed-badge-test {
32+
margin-left: 8px;
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react'
2+
import { IconGeneralSparkleSingleSmall } from '@cypress-design/react-icon'
3+
import cs from 'classnames'
4+
5+
export const SelfHealedBadge = ({ source }: { source: 'command' | 'test' }) => {
6+
return (
7+
<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}`}>
8+
<IconGeneralSparkleSingleSmall strokeColor='jade-300' fillColor='gray-1000' />
9+
<span>
10+
Self-healed
11+
</span>
12+
</div>
13+
)
14+
}

packages/reporter/src/main-runner.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@import 'lib/switch';
77
@import 'lib/tag';
88
@import 'lib/tooltip';
9+
@import 'lib/selfHealedBadge';
910
@import '@reach/dialog/styles.css';
1011
// import all other scss files in src except if they are in lib
1112
// or their file name is `selector-playground` or `main`

packages/reporter/src/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
@import 'lib/switch';
1313
@import 'lib/tag';
1414
@import 'lib/tooltip';
15+
@import 'lib/selfHealedBadge';
1516
@import '@reach/dialog/styles.css';
1617
// import all other scss files in src except if they are in lib
1718
// or their file name is `selector-playground` or `main`

0 commit comments

Comments
 (0)