Skip to content
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ _Released 9/23/2025_
**Bugfixes:**

- In development mode, Electron `stderr` is piped directly to Cypress' `stderr` to make it clear why Electron failed to start, if it fails to start. Fixes [#32358](https://github.com/cypress-io/cypress/issues/32358). Addressed in [32468](https://github.com/cypress-io/cypress/pull/32468).
- Fixed Next button placement and behavior during test stepping workflow. The button now maintains consistent visibility during stepping sessions, staying visible but disabled when no immediate next command is available, providing clear visual feedback to users about stepping state. Fixes [#32476](https://github.com/cypress-io/cypress/issues/32476). Fixed in [#32536](https://github.com/cypress-io/cypress/pull/32536).
- Fixed an issue where ESM Cypress configurations were not being interpreted correctly. Fixes [#32493](https://github.com/cypress-io/cypress/issues/32493). Fixed in [#32515](https://github.com/cypress-io/cypress/pull/32515).

**Misc:**
Expand Down
69 changes: 68 additions & 1 deletion packages/reporter/cypress/e2e/header.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,79 @@ describe('header', () => {
cy.get('.restart').should('not.exist')
})

it('does not display next button', () => {
it('does not displays the next button', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('does not displays the next button', () => {
it('does not display the next button', () => {

cy.get('.next').should('not.exist')
})
})
})

describe('when running after resume', () => {
beforeEach(() => {
runner.emit('run:start')
runner.emit('paused')
cy.get('.play').click()
})

it('displays next button as disabled when running after resume', () => {
cy.get('.next').should('be.visible').and('be.disabled')
})

it('shows "Step (not available)" tooltip when running after resume', () => {
cy.get('.next').trigger('mouseover', { force: true })
cy.get('.cy-tooltip').should('have.text', 'Step (not available)')
})

it('does not emit runner:next when disabled button is clicked', () => {
cy.spy(runner, 'emit')
cy.get('.next').click({ force: true })
cy.wrap(runner.emit).should('not.be.calledWith', 'runner:next')
})

it('displays stop button when running after resume', () => {
cy.get('.stop').should('be.visible')
})
})

describe('when paused without next command', () => {
beforeEach(() => {
runner.emit('paused')
})

it('displays play button', () => {
cy.get('.play').should('be.visible')
})

it('displays tooltip for play button', () => {
cy.get('.play').trigger('mouseover')
cy.get('.cy-tooltip').should('have.text', 'Resume C')
})

it('emits runner:resume when play button is clicked', () => {
cy.spy(runner, 'emit')
cy.get('.play').click()
cy.wrap(runner.emit).should('be.calledWith', 'runner:resume')
})

it('displays next button', () => {
cy.get('.next').should('be.visible').and('be.disabled')
})

it('shows "Step (not available)" tooltip when next button is disabled', () => {
cy.get('.next').trigger('mouseover', { force: true })
cy.get('.cy-tooltip').should('have.text', 'Step (not available)')
})

it('does not emit runner:next when disabled next button is clicked', () => {
cy.spy(runner, 'emit')
cy.get('.next').click({ force: true })
cy.wrap(runner.emit).should('not.be.calledWith', 'runner:next')
})

it('does not display stop button', () => {
cy.get('.stop').should('not.exist')
})
})

describe('when paused with next command', () => {
beforeEach(() => {
runner.emit('paused', 'find')
Expand Down
23 changes: 19 additions & 4 deletions packages/reporter/src/header/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,26 @@ const Controls: React.FC<Props> = observer(({ events = defaultEvents, appState,
</div>
</Tooltip>
)}
{!!appState.nextCommandName && (
<Tooltip placement='bottom' title={<p>Next <span className='kbd'>[N]:</span>{appState.nextCommandName}</p>} className='cy-tooltip'>
{(appState.isPaused || (appState.isRunning && appState.isResumed)) && (
<Tooltip
placement='bottom'
title={appState.nextCommandName ? <p>Next <span className='kbd'>[N]:</span>{appState.nextCommandName}</p> : <p>Step (not available)</p>}
className='cy-tooltip'
>
<div>
<Button size='20' variant='outline-dark' aria-label={`Next '${appState.nextCommandName}'`} className='next' onClick={emit('next')}>
<IconActionNext size='16' strokeColor={iconStrokeColor} fillColor={iconFillColor} />
<Button
size='20'
variant='outline-dark'
aria-label={appState.nextCommandName ? `Next '${appState.nextCommandName}'` : 'Next (not available)'}
className='next disabled:hover:border-white/20 disabled:focus:border-white/20'
disabled={!appState.nextCommandName}
onClick={appState.nextCommandName ? emit('next') : () => { }}
>
<IconActionNext
size='16'
strokeColor={(appState.nextCommandName) ? iconStrokeColor : 'gray-700'}
fillColor={iconFillColor}
/>
</Button>
</div>
</Tooltip>
Expand Down
5 changes: 5 additions & 0 deletions packages/reporter/src/lib/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface DefaultAppState {
pinnedSnapshotId: number | string | null
studioActive: boolean
studioSingleTestActive: boolean
isResumed: boolean
}

// these are used for the `reset` method
Expand All @@ -21,6 +22,7 @@ const defaults: DefaultAppState = {
pinnedSnapshotId: null,
studioActive: false,
studioSingleTestActive: false,
isResumed: false,
}

class AppState {
Expand All @@ -35,6 +37,7 @@ class AppState {
studioActive = defaults.studioActive
studioSingleTestActive = defaults.studioSingleTestActive
isStopped = false
isResumed = defaults.isResumed
_resetAutoScrollingEnabledTo = true;
[key: string]: any

Expand All @@ -50,6 +53,7 @@ class AppState {
pinnedSnapshotId: observable,
studioActive: observable,
studioSingleTestActive: observable,
isResumed: observable,
})
}

Expand All @@ -66,6 +70,7 @@ class AppState {
resume () {
this.isPaused = false
this.nextCommandName = null
this.isResumed = true
}

stop () {
Expand Down
Loading