Skip to content

Commit 2a10b4f

Browse files
misc: Add keyboard shortcut to keyboard shortcut modal for saving studio changes (#32864)
* misc: Add keyboard shortcut to keyboard shortcut modal for saving studio changes * Add changelog entry * move changelog to new release entry * add pending to changelog * remove duplicate changelog entry
1 parent 9b5df82 commit 2a10b4f

File tree

4 files changed

+195
-28
lines changed

4 files changed

+195
-28
lines changed

cli/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
2+
## 15.6.1
3+
4+
_Released 11/18/2025 (PENDING)_
5+
6+
**Misc:**
7+
8+
- 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).
9+
210
## 15.6.0
311

412
_Released 11/4/2025_
@@ -23,7 +31,6 @@ _Released 11/4/2025_
2331
- 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).
2432
- 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).
2533
- 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).
26-
- The 'Next' tooltip style was updated. Addressed in [#32866](https://github.com/cypress-io/cypress/pull/32866).
2734
- 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)
2835
- 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).
2936

packages/app/src/navigation/KeyboardBindingsModal.cy.tsx

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,143 @@ import KeyboardBindingsModal from './KeyboardBindingsModal.vue'
22
// tslint:disable-next-line: no-implicit-dependencies - unsure how to handle these
33
import { defaultMessages } from '@cy/i18n'
44

5+
const setPlatformConfig = (platform: string) => {
6+
return cy.window().then((win) => {
7+
// @ts-ignore
8+
win.__CYPRESS_CONFIG__ = {
9+
base64Config: Cypress.Buffer.from(JSON.stringify({ platform })).toString('base64'),
10+
}
11+
})
12+
}
13+
514
describe('KeyboardBindingsModal', () => {
6-
it('renders expected content', () => {
7-
cy.mount(() => {
8-
return <KeyboardBindingsModal show />
15+
describe('rendering', () => {
16+
it('renders expected content', () => {
17+
cy.mount(() => {
18+
return <KeyboardBindingsModal show />
19+
})
20+
21+
const expectedContent = defaultMessages.sidebar.keyboardShortcuts
22+
23+
Object.values(expectedContent).forEach((text) => {
24+
cy.contains(text).should('be.visible')
25+
})
26+
})
27+
28+
it('renders all keyboard bindings with their keys', () => {
29+
cy.mount(() => {
30+
return <KeyboardBindingsModal show />
31+
})
32+
33+
// Check that all keyboard shortcuts are displayed with their keys
34+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.rerun).should('be.visible')
35+
cy.contains('r').should('be.visible')
36+
37+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.stop).should('be.visible')
38+
cy.contains('s').should('be.visible')
39+
40+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.toggle).should('be.visible')
41+
cy.contains('f').should('be.visible')
42+
43+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
44+
})
45+
46+
it('does not render when show is false', () => {
47+
cy.mount(() => {
48+
return <KeyboardBindingsModal show={false} />
49+
})
50+
51+
cy.get('[data-cy="keyboard-modal"]').should('not.exist')
52+
})
53+
})
54+
55+
describe('platform-specific keyboard shortcuts', () => {
56+
it('shows ⌘+s on macOS (darwin)', () => {
57+
setPlatformConfig('darwin').then(() => {
58+
cy.mount(() => {
59+
return <KeyboardBindingsModal show />
60+
})
61+
62+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
63+
cy.contains('⌘').should('be.visible')
64+
cy.contains('+').should('be.visible')
65+
cy.contains('s').should('be.visible')
66+
cy.contains('Ctrl').should('not.exist')
67+
68+
cy.percySnapshot()
69+
})
70+
})
71+
72+
it('shows Ctrl+s on Windows', () => {
73+
setPlatformConfig('win32').then(() => {
74+
cy.mount(() => {
75+
return <KeyboardBindingsModal show />
76+
})
77+
78+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
79+
cy.contains('Ctrl').should('be.visible')
80+
cy.contains('+').should('be.visible')
81+
cy.contains('s').should('be.visible')
82+
cy.contains('⌘').should('not.exist')
83+
84+
cy.percySnapshot()
85+
})
986
})
1087

11-
const expectedContent = defaultMessages.sidebar.keyboardShortcuts
88+
it('shows Ctrl+s on Linux', () => {
89+
setPlatformConfig('linux').then(() => {
90+
cy.mount(() => {
91+
return <KeyboardBindingsModal show />
92+
})
93+
94+
cy.contains(defaultMessages.sidebar.keyboardShortcuts.studioSave).should('be.visible')
95+
cy.contains('Ctrl').should('be.visible')
96+
cy.contains('+').should('be.visible')
97+
cy.contains('s').should('be.visible')
98+
cy.contains('⌘').should('not.exist')
99+
})
100+
})
101+
102+
it('falls back to darwin if platform is not available', () => {
103+
cy.window().then((win) => {
104+
// @ts-ignore
105+
win.__CYPRESS_CONFIG__ = undefined
106+
}).then(() => {
107+
cy.mount(() => {
108+
return <KeyboardBindingsModal show />
109+
})
110+
111+
// Should fallback to darwin and show ⌘
112+
cy.contains('⌘').should('be.visible')
113+
cy.contains('Ctrl').should('not.exist')
114+
})
115+
})
116+
})
117+
118+
describe('modal behavior', () => {
119+
it('emits close event when close button is clicked', () => {
120+
const closeSpy = cy.stub().as('closeSpy')
121+
122+
cy.mount(() => {
123+
return <KeyboardBindingsModal show onClose={closeSpy} />
124+
})
125+
126+
cy.get('[data-cy="keyboard-modal"]').should('be.visible')
127+
cy.findByLabelText('Close').click()
128+
cy.get('@closeSpy').should('have.been.calledOnce')
129+
})
130+
131+
it('emits close event when clicking outside the modal', () => {
132+
const closeSpy = cy.stub().as('closeSpy')
133+
134+
cy.mount(() => {
135+
return <KeyboardBindingsModal show onClose={closeSpy} />
136+
})
12137

13-
Object.values(expectedContent).forEach((text) => {
14-
cy.contains(text).should('be.visible')
138+
cy.get('[data-cy="keyboard-modal"]').should('be.visible')
139+
// Click outside the modal (on the backdrop)
140+
cy.get('body').click(0, 0)
141+
cy.get('@closeSpy').should('have.been.calledOnce')
15142
})
16143
})
17144
})

packages/app/src/navigation/KeyboardBindingsModal.vue

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,36 @@
1717
<p class="grow text-gray-700 text-[16px] leading-[24px]">
1818
{{ binding.description }}
1919
</p>
20-
<span
21-
v-for="key in binding.key"
22-
:key="key"
23-
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"
20+
<template
21+
v-for="(key, index) in binding.key"
22+
:key="`${binding.key.join('-')}-${index}`"
2423
>
25-
{{ key }}
26-
</span>
24+
<span
25+
v-if="key === '+'"
26+
class="mx-[4px] text-gray-700 text-[14px] leading-[20px]"
27+
>
28+
{{ key }}
29+
</span>
30+
<span
31+
v-else
32+
:class="[
33+
'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',
34+
index > 0 && binding.key[index - 1] === '+' ? 'ml-[4px]' : 'ml-[8px]'
35+
]"
36+
>
37+
{{ key }}
38+
</span>
39+
</template>
2740
</li>
2841
</ul>
2942
</StandardModal>
3043
</template>
3144

3245
<script lang="ts" setup>
46+
import { computed } from 'vue'
3347
import StandardModal from '@cy/components/StandardModal.vue'
3448
import { useI18n } from '@cy/i18n'
49+
import { getRunnerConfigFromWindow } from '../runner/get-runner-config-from-window'
3550
3651
const { t } = useI18n()
3752
@@ -43,18 +58,35 @@ const emits = defineEmits<{
4358
(eventName: 'close'): void
4459
}>()
4560
46-
const keyBindings = [
47-
{
48-
key: ['r'],
49-
description: t('sidebar.keyboardShortcuts.rerun'),
50-
},
51-
{
52-
key: ['s'],
53-
description: t('sidebar.keyboardShortcuts.stop'),
54-
},
55-
{
56-
key: ['f'],
57-
description: t('sidebar.keyboardShortcuts.toggle'),
58-
},
59-
]
61+
const platform = computed(() => {
62+
try {
63+
return getRunnerConfigFromWindow().platform
64+
} catch {
65+
// Fallback to darwin if platform is not available (e.g., during tests)
66+
return 'darwin'
67+
}
68+
})
69+
70+
const isDarwin = computed(() => platform.value === 'darwin')
71+
72+
const keyBindings = computed(() => {
73+
return [
74+
{
75+
key: ['r'],
76+
description: t('sidebar.keyboardShortcuts.rerun'),
77+
},
78+
{
79+
key: ['s'],
80+
description: t('sidebar.keyboardShortcuts.stop'),
81+
},
82+
{
83+
key: ['f'],
84+
description: t('sidebar.keyboardShortcuts.toggle'),
85+
},
86+
{
87+
key: isDarwin.value ? ['', '+', 's'] : ['Ctrl', '+', 's'],
88+
description: t('sidebar.keyboardShortcuts.studioSave'),
89+
},
90+
]
91+
})
6092
</script>

packages/frontend-shared/src/locales/en-US.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@
285285
"title": "Keyboard shortcuts",
286286
"rerun": "Re-run tests",
287287
"stop": "Stop tests",
288-
"toggle": "Toggle specs list"
288+
"toggle": "Toggle specs list",
289+
"studioSave": "Save Studio changes"
289290
},
290291
"toggleLabel": {
291292
"expanded": "Collapse sidebar",

0 commit comments

Comments
 (0)