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
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ _Released 11/18/2025 (PENDING)_
**Misc:**

- The keyboard shortcuts modal now displays the keyboard shortcut for saving Studio changes - `⌘` + `s` for Mac or `Ctrl` + `s` for Windows/Linux. Addressed [#32862](https://github.com/cypress-io/cypress/issues/32862). Addressed in [#32864](https://github.com/cypress-io/cypress/pull/32864).
- Popup tooltip guide when opening the welcome to studio panel. Addresses [#11906](https://github.com/cypress-io/cypress-services/issues/11906). Addressed in [#32905](https://github.com/cypress-io/cypress/pull/32905).

## 15.6.0

Expand Down
4 changes: 4 additions & 0 deletions packages/app/cypress/e2e/studio/studio-new-tests.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ describe('Cypress Studio - New Test Creation', () => {

inputNewTestName({ creatingNewTestFromWelcomeScreen: false })

cy.findByTestId('studio-tooltip-guide').should('not.exist')

cy.contains('new-test').click()

cy.percySnapshot()
Expand Down Expand Up @@ -130,6 +132,8 @@ describe('studio functionality', () => {

inputNewTestName({ creatingNewTestFromWelcomeScreen: false })

cy.findByTestId('studio-tooltip-guide').should('not.exist')

// make sure that the visit has run and we're recording studio commands
cy.get('[data-cy="record-button-recording"]').should('be.visible')

Expand Down
3 changes: 3 additions & 0 deletions packages/app/cypress/e2e/studio/studio-ui.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ describe('studio functionality', () => {
cy.findByTestId('studio-button').should('be.visible').click()
cy.findByTestId('studio-panel').should('be.visible')

// studio guide tooltip should be visible
cy.findByTestId('studio-tooltip-guide').should('be.visible')

cy.contains('New test')

cy.percySnapshot()
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/runner/SpecRunnerOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ fragment SpecRunner_Preferences on Query {
reporterWidth
specListWidth
studioWidth
studioTooltipDismissed
}
}
}
Expand Down Expand Up @@ -361,6 +362,8 @@ preferences.update('autoScrollingEnabled', props.gql.localSettings.preferences.a

preferences.update('showFetchRequests', props.gql.localSettings.preferences.showFetchRequests ?? true)

preferences.update('studioTooltipDismissed', props.gql.localSettings.preferences.studioTooltipDismissed ?? false)

// if the CYPRESS_NO_COMMAND_LOG environment variable is set,
// don't use the widths or the open status of specs list from GraphQL
if (!hideCommandLog) {
Expand Down Expand Up @@ -444,6 +447,7 @@ onMounted(() => {
preferences.update('isSpecsListOpen', state.isSpecsListOpen)
preferences.update('autoScrollingEnabled', state.autoScrollingEnabled)
preferences.update('showFetchRequests', state.showFetchRequests)
preferences.update('studioTooltipDismissed', state.studioTooltipDismissed)
})
})

Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export class EventManager {
this.studioStore.setCanAccessStudioAI(canAccessStudioAI)
this.studioStore.setSessionId(cloudStudioSessionId)
this.studioStore.setActive(true)
this.reporterBus.emit('reporter:set:studio:welcome:panel:active', entrySource === 'welcome')
})
}

Expand All @@ -310,6 +311,7 @@ export class EventManager {
const needsReload = this.studioStore.needsProtocolCleanup()

this.studioStore.cancel()
this.reporterBus.emit('reporter:set:studio:welcome:panel:active', false)

// only reload the page if Studio has actually been used for recording
if (needsReload) {
Expand Down Expand Up @@ -883,6 +885,7 @@ export class EventManager {
!!this.studioStore.newTestLineNumber

const studioSingleTestActive = this.studioStore.newTestLineNumber != null || !!this.studioStore.testId
const isStudioWelcomePanelActive = this.studioStore.isActive && !!this.studioStore.suiteId && (this.studioStore.entrySource === 'welcome' || !this.studioStore.entrySource)

this.reporterBus.emit('reporter:start', {
startTime: Cypress.runner.getStartTime(),
Expand All @@ -895,6 +898,7 @@ export class EventManager {
scrollTop: runState.scrollTop,
studioActive: hasActiveStudio,
studioSingleTestActive,
isStudioWelcomePanelActive,
} as ReporterStartInfo)
}

Expand Down
1 change: 1 addition & 0 deletions packages/app/src/runner/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function renderReporter (
autoScrollingEnabled: runnerUiStore.autoScrollingEnabled,
isSpecsListOpen: runnerUiStore.isSpecsListOpen,
showFetchRequests: runnerUiStore.showFetchRequests,
studioTooltipDismissed: runnerUiStore.studioTooltipDismissed,
error: null,
resetStatsOnSpecChange: true,
// Studio can only be enabled for e2e testing
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/store/runner-ui-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface RunnerUiState {
autoScrollingEnabled: boolean
isSpecsListOpen: boolean
showFetchRequests: boolean
studioTooltipDismissed: boolean
specListWidth: number
reporterWidth: number
studioWidth: number
Expand All @@ -41,6 +42,7 @@ export const useRunnerUiStore = defineStore({
autoScrollingEnabled: true,
isSpecsListOpen: false,
showFetchRequests: true,
studioTooltipDismissed: false,
specListWidth: runnerConstants.defaultSpecListWidth,
reporterWidth: runnerConstants.defaultReporterWidth,
studioWidth: runnerConstants.defaultStudioWidth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export const LocalSettingsPreferences = objectType({
return ctx.coreData.localSettings.preferences.notifyWhenRunCompletes || []
},
})

t.boolean('studioTooltipDismissed')
},
})

Expand Down
1 change: 1 addition & 0 deletions packages/data-context/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,7 @@ type LocalSettingsPreferences {
shouldLaunchBrowserFromOpenBrowser: Boolean
showFetchRequests: Boolean
specListWidth: Int
studioTooltipDismissed: Boolean
studioWidth: Int
wasBrowserSetInCLI: Boolean
}
Expand Down
72 changes: 70 additions & 2 deletions packages/reporter/cypress/e2e/tests.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store'
let runner: EventEmitter
let runnables: RootRunnable

function visitAndRenderReporter (studioEnabled: boolean = false, studioActive: boolean = false, specRelative: string = 'relative/path/to/foo.js') {
function visitAndRenderReporter (studioEnabled: boolean = false, studioActive: boolean = false, specRelative: string = 'relative/path/to/foo.js', isStudioWelcomePanelActive: boolean = false) {
cy.fixture('runnables').then((_runnables) => {
runnables = _runnables
})
Expand All @@ -30,7 +30,7 @@ function visitAndRenderReporter (studioEnabled: boolean = false, studioActive: b

cy.get('.reporter.mounted').then(() => {
runner.emit('runnables:ready', runnables)
runner.emit('reporter:start', { studioActive })
runner.emit('reporter:start', { studioActive, isStudioWelcomePanelActive })
})

return runnerStore
Expand Down Expand Up @@ -265,4 +265,72 @@ describe('studio controls', () => {
cy.wrap(runner.emit).should('be.calledWith', 'studio:init:test', { testId: 'r3' })
})
})

describe('display studio tooltip guide for new test page', () => {
beforeEach(() => {
const runnerStore = visitAndRenderReporter(true, false, 'relative/path/to/foo.js', true)

runnerStore.setCanSaveStudioLogs(false)
})

const assertNewTestPageTooltip = () => {
// studio tooltip guide should be displayed for the first test in the new test page
cy.get('.test').first().within(() => {
cy.get('[data-cy="studio-tooltip-guide"]').should('exist')
})

cy.get('.cy-tooltip').first().contains('Edit test in studio')
cy.get('.cy-tooltip').first().contains('Open a test in Studio to make edits')
cy.get('.cy-tooltip').first().contains('Refine test with AI recommendations')
}

const assertStudioTooltipDismissed = () => {
cy.wrap(runner.emit).should('be.calledWith', 'save:state')
cy.get('[data-cy="studio-tooltip-guide"]').should('not.exist')
}

it('displays studio tooltip guide for new test page', () => {
// when just entering the new test page, the tooltip should be displayed
cy.get('.cy-tooltip').should('have.length', 1)
assertNewTestPageTooltip()

// when hovering over other tests, the Edit in Studio tooltip should be displayed as well
cy.contains('failed with retries')
.closest('.collapsible-header')
.find('.runnable-controls-studio')
.realHover()
.should('be.visible')
.should('have.css', 'opacity', '1')

cy.get('.cy-tooltip').should('have.length', 2)
assertNewTestPageTooltip()
cy.get('.cy-tooltip').eq(1).contains('Edit in Studio')
})

it('dismisses studio tooltip guide with dismiss icon for new test page', () => {
cy.stub(runner, 'emit')
assertNewTestPageTooltip()

cy.get('[data-cy="dismiss-studio-tooltip-icon"]').click()
assertStudioTooltipDismissed()
})

it('dismisses studio tooltip guide with got it button for new test page', () => {
cy.stub(runner, 'emit')

assertNewTestPageTooltip()
cy.get('[data-cy="got-it-dont-show-again-button"]').click()
assertStudioTooltipDismissed()
})

it('dismisses studio tooltip guide when clicking on the test', () => {
cy.stub(runner, 'emit')

assertNewTestPageTooltip()

cy.get('[data-cy="studio-tooltip-guide"]').click({ force: true })
assertStudioTooltipDismissed()
cy.wrap(runner.emit).should('be.calledWith', 'studio:init:test', { testId: 'r3' })
})
})
})
11 changes: 11 additions & 0 deletions packages/reporter/cypress/e2e/unit/app_state.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,15 @@ describe('app state', () => {
expect(instance.studioActive).to.be.false
})
})

context('#setStudioTooltipDismissed', () => {
it('sets studioTooltipDismissed', () => {
const instance = new AppState()

expect(instance.studioTooltipDismissed).to.eq(false)

instance.setStudioTooltipDismissed(true)
expect(instance.studioTooltipDismissed).to.eq(true)
})
})
})
10 changes: 10 additions & 0 deletions packages/reporter/cypress/e2e/unit/events.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type AppStateStub = AppState & {
temporarilySetAutoScrolling: SinonSpy
setStudioActive: SinonSpy
setStudioSingleTestActive: SinonSpy
setIsStudioWelcomePanelActive: SinonSpy
stop: SinonSpy
}

Expand All @@ -41,6 +42,7 @@ const appStateStub = () => {
setStudioActive: sinon.spy(),
setStudioSingleTestActive: sinon.spy(),
stop: sinon.spy(),
setIsStudioWelcomePanelActive: sinon.spy(),
} as AppStateStub
}

Expand Down Expand Up @@ -265,6 +267,12 @@ describe('events', () => {
runner.on.withArgs('reporter:snapshot:unpinned').callArgWith(1)
expect(appState.pinnedSnapshotId).to.be.null
})

it('sets isStudioWelcomePanelActive on the app state on reporter:set:studio:welcome:panel:active', () => {
appState.isStudioWelcomePanelActive = false
runner.on.withArgs('reporter:set:studio:welcome:panel:active').callArgWith(1, true)
expect(appState.setIsStudioWelcomePanelActive).to.have.been.calledWith(true)
})
})

context('from local bus', () => {
Expand Down Expand Up @@ -355,11 +363,13 @@ describe('events', () => {
appState.autoScrollingUserPref = false
appState.isSpecsListOpen = true
appState.showFetchRequests = false
appState.studioTooltipDismissed = true
events.emit('save:state')
expect(runner.emit).to.have.been.calledWith('save:state', {
autoScrollingEnabled: false,
isSpecsListOpen: true,
showFetchRequests: false,
studioTooltipDismissed: true,
})
})

Expand Down
14 changes: 10 additions & 4 deletions packages/reporter/src/components/LaunchStudioIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@ import React, { MouseEvent } from 'react'

import Tooltip from '@cypress/react-tooltip'
import { IconChevronRightMedium } from '@cypress-design/react-icon'
import cx from 'classnames'

interface LaunchStudioIconProps {
content: React.ReactNode
onClick: (e: MouseEvent) => void
wrapperClassName?: string
className?: string
visible?: boolean
dataCy?: string
}

export const LaunchStudioIcon: React.FC<LaunchStudioIconProps> = ({ content, onClick }) => {
export const LaunchStudioIcon: React.FC<LaunchStudioIconProps> = ({ content, onClick, className, wrapperClassName, visible, dataCy = 'launch-studio' }) => {
return (
<Tooltip
placement='right'
className='cy-tooltip'
className={cx(className, 'cy-tooltip')}
title={content}
visible={visible}
>
<a
onClick={onClick}
className='runnable-controls-studio'
data-cy='launch-studio'
className={cx('runnable-controls-studio', wrapperClassName)}
data-cy={dataCy}
>
<IconChevronRightMedium style={{ marginTop: '-1px' }} />
</a>
Expand Down
14 changes: 14 additions & 0 deletions packages/reporter/src/lib/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface DefaultAppState {
pinnedSnapshotId: number | string | null
studioActive: boolean
studioSingleTestActive: boolean
isStudioWelcomePanelActive: boolean
hasBeenPaused: boolean
}

Expand All @@ -20,6 +21,7 @@ const defaults: DefaultAppState = {
pinnedSnapshotId: null,
studioActive: false,
studioSingleTestActive: false,
isStudioWelcomePanelActive: false,
hasBeenPaused: false,
}

Expand All @@ -33,9 +35,11 @@ class AppState {
pinnedSnapshotId = defaults.pinnedSnapshotId
studioActive = defaults.studioActive
studioSingleTestActive = defaults.studioSingleTestActive
isStudioWelcomePanelActive = defaults.isStudioWelcomePanelActive
showFetchRequests = true
isStopped = false
hasBeenPaused = defaults.hasBeenPaused
studioTooltipDismissed = false
_resetAutoScrollingEnabledTo = true;
[key: string]: any

Expand All @@ -50,8 +54,10 @@ class AppState {
pinnedSnapshotId: observable,
studioActive: observable,
studioSingleTestActive: observable,
isStudioWelcomePanelActive: observable,
showFetchRequests: observable,
hasBeenPaused: observable,
studioTooltipDismissed: observable,
})
}

Expand Down Expand Up @@ -134,6 +140,10 @@ class AppState {
this.studioSingleTestActive = studioSingleTestActive
}

setIsStudioWelcomePanelActive (isStudioWelcomePanelActive: boolean) {
this.isStudioWelcomePanelActive = isStudioWelcomePanelActive
}

toggleShowFetchRequests () {
this.showFetchRequests = !this.showFetchRequests
}
Expand All @@ -142,6 +152,10 @@ class AppState {
this.showFetchRequests = showFetchRequests
}

setStudioTooltipDismissed (dismissed: boolean) {
this.studioTooltipDismissed = dismissed
}

reset () {
_.each(defaults, (value: any, key: string) => {
this[key] = value
Expand Down
Loading
Loading