Skip to content
Merged
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ _Released 11/4/2025 (PENDING)_
- The hitbox for expanding a grouped command has been widened. Addresses [#32778](https://github.com/cypress-io/cypress/issues/32778). Addressed in [#32783](https://github.com/cypress-io/cypress/pull/32783).
- Have cursor on hover of the AUT URL to show as pointer. Addresses [#32777](https://github.com/cypress-io/cypress/issues/32777). Addressed in [#32782](https://github.com/cypress-io/cypress/pull/32782).
- WebKit now prefers a cookie's fully qualified `domain` when requesting a cookie value via [`cy.getCookie()`](https://docs.cypress.io/api/commands/getcookie). If none are found, the cookie's apex domain will be used as a fallback. Addresses [#29954](https://github.com/cypress-io/cypress/issues/29954), [#29973](https://github.com/cypress-io/cypress/issues/29973) and [#30392](https://github.com/cypress-io/cypress/issues/30392). Addressed in [#32852](https://github.com/cypress-io/cypress/pull/32852).
- 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).
Copy link

Choose a reason for hiding this comment

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

Bug: Duplicate changelog entry across releases.

The changelog entry for keyboard shortcuts modal is duplicated in both version 15.6.1 (lines 7-8) and version 15.6.0 (line 34). Since version 15.6.0 was already released on 11/4/2025, this change cannot be part of that version. The entry should only appear in version 15.6.1, not in the already-released 15.6.0 section.

Fix in Cursor Fix in Web

- The 'Next' tooltip style was updated. Addressed in [#32866](https://github.com/cypress-io/cypress/pull/32866).

**Dependency Updates:**
Expand Down
139 changes: 133 additions & 6 deletions packages/app/src/navigation/KeyboardBindingsModal.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,143 @@ import KeyboardBindingsModal from './KeyboardBindingsModal.vue'
// tslint:disable-next-line: no-implicit-dependencies - unsure how to handle these
import { defaultMessages } from '@cy/i18n'

const setPlatformConfig = (platform: string) => {
return cy.window().then((win) => {
// @ts-ignore
win.__CYPRESS_CONFIG__ = {
base64Config: Cypress.Buffer.from(JSON.stringify({ platform })).toString('base64'),
}
})
}

describe('KeyboardBindingsModal', () => {
it('renders expected content', () => {
cy.mount(() => {
return <KeyboardBindingsModal show />
describe('rendering', () => {
it('renders expected content', () => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

const expectedContent = defaultMessages.sidebar.keyboardShortcuts

Object.values(expectedContent).forEach((text) => {
cy.contains(text).should('be.visible')
})
})

it('renders all keyboard bindings with their keys', () => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

// Check that all keyboard shortcuts are displayed with their keys
cy.contains(defaultMessages.sidebar.keyboardShortcuts.rerun).should('be.visible')
cy.contains('r').should('be.visible')

cy.contains(defaultMessages.sidebar.keyboardShortcuts.stop).should('be.visible')
cy.contains('s').should('be.visible')

cy.contains(defaultMessages.sidebar.keyboardShortcuts.toggle).should('be.visible')
cy.contains('f').should('be.visible')

cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
})

it('does not render when show is false', () => {
cy.mount(() => {
return <KeyboardBindingsModal show={false} />
})

cy.get('[data-cy="keyboard-modal"]').should('not.exist')
})
})

describe('platform-specific keyboard shortcuts', () => {
it('shows ⌘+s on macOS (darwin)', () => {
setPlatformConfig('darwin').then(() => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
cy.contains('⌘').should('be.visible')
cy.contains('+').should('be.visible')
cy.contains('s').should('be.visible')
cy.contains('Ctrl').should('not.exist')

cy.percySnapshot()
})
})

it('shows Ctrl+s on Windows', () => {
setPlatformConfig('win32').then(() => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
cy.contains('Ctrl').should('be.visible')
cy.contains('+').should('be.visible')
cy.contains('s').should('be.visible')
cy.contains('⌘').should('not.exist')

cy.percySnapshot()
})
})

const expectedContent = defaultMessages.sidebar.keyboardShortcuts
it('shows Ctrl+s on Linux', () => {
setPlatformConfig('linux').then(() => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
cy.contains('Ctrl').should('be.visible')
cy.contains('+').should('be.visible')
cy.contains('s').should('be.visible')
cy.contains('⌘').should('not.exist')
})
})

it('falls back to darwin if platform is not available', () => {
cy.window().then((win) => {
// @ts-ignore
win.__CYPRESS_CONFIG__ = undefined
}).then(() => {
cy.mount(() => {
return <KeyboardBindingsModal show />
})

// Should fallback to darwin and show ⌘
cy.contains('⌘').should('be.visible')
cy.contains('Ctrl').should('not.exist')
})
})
})

describe('modal behavior', () => {
it('emits close event when close button is clicked', () => {
const closeSpy = cy.stub().as('closeSpy')

cy.mount(() => {
return <KeyboardBindingsModal show onClose={closeSpy} />
})

cy.get('[data-cy="keyboard-modal"]').should('be.visible')
cy.findByLabelText('Close').click()
cy.get('@closeSpy').should('have.been.calledOnce')
})

it('emits close event when clicking outside the modal', () => {
const closeSpy = cy.stub().as('closeSpy')

cy.mount(() => {
return <KeyboardBindingsModal show onClose={closeSpy} />
})

Object.values(expectedContent).forEach((text) => {
cy.contains(text).should('be.visible')
cy.get('[data-cy="keyboard-modal"]').should('be.visible')
// Click outside the modal (on the backdrop)
cy.get('body').click(0, 0)
cy.get('@closeSpy').should('have.been.calledOnce')
})
})
})
72 changes: 52 additions & 20 deletions packages/app/src/navigation/KeyboardBindingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,36 @@
<p class="grow text-gray-700 text-[16px] leading-[24px]">
{{ binding.description }}
</p>
<span
v-for="key in binding.key"
:key="key"
class="border rounded-sm bg-gray-50 border-gray-100 h-[24px] text-center ml-[8px] text-indigo-500 text-[14px] leading-[20px] w-[24px] inline-block"
<template
v-for="(key, index) in binding.key"
:key="`${binding.key.join('-')}-${index}`"
>
{{ key }}
</span>
<span
v-if="key === '+'"
class="mx-[4px] text-gray-700 text-[14px] leading-[20px]"
>
{{ key }}
</span>
<span
v-else
:class="[
'border rounded-sm bg-gray-50 border-gray-100 h-[24px] text-center text-indigo-500 text-[14px] leading-[20px] min-w-[24px] px-[6px] inline-flex items-center justify-center',
index > 0 && binding.key[index - 1] === '+' ? 'ml-[4px]' : 'ml-[8px]'
]"
>
{{ key }}
</span>
</template>
</li>
</ul>
</StandardModal>
</template>

<script lang="ts" setup>
import { computed } from 'vue'
import StandardModal from '@cy/components/StandardModal.vue'
import { useI18n } from '@cy/i18n'
import { getRunnerConfigFromWindow } from '../runner/get-runner-config-from-window'

const { t } = useI18n()

Expand All @@ -43,18 +58,35 @@ const emits = defineEmits<{
(eventName: 'close'): void
}>()

const keyBindings = [
{
key: ['r'],
description: t('sidebar.keyboardShortcuts.rerun'),
},
{
key: ['s'],
description: t('sidebar.keyboardShortcuts.stop'),
},
{
key: ['f'],
description: t('sidebar.keyboardShortcuts.toggle'),
},
]
const platform = computed(() => {
try {
return getRunnerConfigFromWindow().platform
} catch {
// Fallback to darwin if platform is not available (e.g., during tests)
return 'darwin'
}
})

const isDarwin = computed(() => platform.value === 'darwin')

const keyBindings = computed(() => {
return [
{
key: ['r'],
description: t('sidebar.keyboardShortcuts.rerun'),
},
{
key: ['s'],
description: t('sidebar.keyboardShortcuts.stop'),
},
{
key: ['f'],
description: t('sidebar.keyboardShortcuts.toggle'),
},
{
key: isDarwin.value ? ['⌘', '+', 's'] : ['Ctrl', '+', 's'],
description: t('sidebar.keyboardShortcuts.studioSave'),
},
]
})
</script>
3 changes: 2 additions & 1 deletion packages/frontend-shared/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@
"title": "Keyboard shortcuts",
"rerun": "Re-run tests",
"stop": "Stop tests",
"toggle": "Toggle specs list"
"toggle": "Toggle specs list",
"studioSave": "Save Studio changes"
},
"toggleLabel": {
"expanded": "Collapse sidebar",
Expand Down
Loading