diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 2daafdb6f64..9c10d335507 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -1,4 +1,12 @@
+## 15.6.1
+
+_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).
+
## 15.6.0
_Released 11/4/2025_
@@ -23,7 +31,6 @@ _Released 11/4/2025_
- 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 'Next' tooltip style was updated. Addressed in [#32866](https://github.com/cypress-io/cypress/pull/32866).
- Make test name header sticky in studio mode and in the tests list. Addresses [#32591](https://github.com/cypress-io/cypress/issues/32591). Addressed in [#32840](https://github.com/cypress-io/cypress/pull/32840)
- The [`cy.exec()`](https://docs.cypress.io/api/commands/exec) type now reflects the correct yielded response type of `exitCode`. Addresses [#32875](https://github.com/cypress-io/cypress/issues/32875). Addressed in [#32885](https://github.com/cypress-io/cypress/pull/32885).
diff --git a/packages/app/src/navigation/KeyboardBindingsModal.cy.tsx b/packages/app/src/navigation/KeyboardBindingsModal.cy.tsx
index 667ed919cd9..c030c79dffa 100644
--- a/packages/app/src/navigation/KeyboardBindingsModal.cy.tsx
+++ b/packages/app/src/navigation/KeyboardBindingsModal.cy.tsx
@@ -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
+ describe('rendering', () => {
+ it('renders expected content', () => {
+ cy.mount(() => {
+ return
+ })
+
+ 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
+ })
+
+ // 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
+ })
+
+ 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
+ })
+
+ 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
+ })
+
+ 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
+ })
+
+ 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
+ })
+
+ // 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
+ })
+
+ 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
+ })
- 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')
})
})
})
diff --git a/packages/app/src/navigation/KeyboardBindingsModal.vue b/packages/app/src/navigation/KeyboardBindingsModal.vue
index 6b02ad8a29a..ab69cde8241 100644
--- a/packages/app/src/navigation/KeyboardBindingsModal.vue
+++ b/packages/app/src/navigation/KeyboardBindingsModal.vue
@@ -17,21 +17,36 @@
{{ binding.description }}
-
- {{ key }}
-
+
+ {{ key }}
+
+
+ {{ key }}
+
+
diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json
index 8a9b600c69c..16766eb0ac5 100644
--- a/packages/frontend-shared/src/locales/en-US.json
+++ b/packages/frontend-shared/src/locales/en-US.json
@@ -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",