Skip to content

Commit 9a16e47

Browse files
committed
test: Add unit tests for shortcut utils
1 parent 5787bd3 commit 9a16e47

File tree

4 files changed

+250
-4
lines changed

4 files changed

+250
-4
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { global } from '@storybook/global';
2+
3+
const { navigator } = global;
4+
5+
export const isMacLike = () =>
6+
navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false;
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { isMacLike } from './platform';
4+
import type { KeyboardEventLike } from './shortcut';
5+
import {
6+
controlOrMetaKey,
7+
controlOrMetaSymbol,
8+
eventMatchesShortcut,
9+
eventToShortcut,
10+
isShortcutTaken,
11+
keyToSymbol,
12+
optionOrAltSymbol,
13+
shortcutMatchesShortcut,
14+
shortcutToHumanString,
15+
} from './shortcut';
16+
17+
// Mock the functions directly
18+
vi.mock('./platform', async () => {
19+
return {
20+
isMacLike: vi.fn(),
21+
};
22+
});
23+
24+
describe('shortcut', () => {
25+
beforeEach(() => {
26+
vi.mocked(isMacLike).mockReset();
27+
});
28+
29+
describe('platform detection', () => {
30+
it('isMacLike can be mocked', () => {
31+
vi.mocked(isMacLike).mockReturnValue(true);
32+
expect(isMacLike()).toBe(true);
33+
34+
vi.mocked(isMacLike).mockReturnValue(false);
35+
expect(isMacLike()).toBe(false);
36+
});
37+
38+
it('controlOrMetaSymbol returns correct symbol based on platform', () => {
39+
// For Mac
40+
vi.mocked(isMacLike).mockReturnValue(true);
41+
expect(controlOrMetaSymbol()).toBe('⌘');
42+
43+
// For non-Mac
44+
vi.mocked(isMacLike).mockReturnValue(false);
45+
expect(controlOrMetaSymbol()).toBe('ctrl');
46+
});
47+
48+
it('controlOrMetaKey returns correct key based on platform', () => {
49+
// For Mac
50+
vi.mocked(isMacLike).mockReturnValue(true);
51+
expect(controlOrMetaKey()).toBe('meta');
52+
53+
// For non-Mac
54+
vi.mocked(isMacLike).mockReturnValue(false);
55+
expect(controlOrMetaKey()).toBe('control');
56+
});
57+
58+
it('optionOrAltSymbol returns correct symbol based on platform', () => {
59+
// For Mac
60+
vi.mocked(isMacLike).mockReturnValue(true);
61+
expect(optionOrAltSymbol()).toBe('⌥');
62+
63+
// For non-Mac
64+
vi.mocked(isMacLike).mockReturnValue(false);
65+
expect(optionOrAltSymbol()).toBe('alt');
66+
});
67+
});
68+
69+
describe('isShortcutTaken', () => {
70+
it('returns true for identical shortcuts', () => {
71+
expect(isShortcutTaken(['alt', 'K'], ['alt', 'K'])).toBe(true);
72+
});
73+
74+
it('returns false for different shortcuts', () => {
75+
expect(isShortcutTaken(['alt', 'K'], ['alt', 'J'])).toBe(false);
76+
expect(isShortcutTaken(['alt', 'K'], ['meta', 'K'])).toBe(false);
77+
expect(isShortcutTaken(['alt', 'K'], ['alt', 'K', 'L'])).toBe(false);
78+
});
79+
});
80+
81+
describe('eventToShortcut', () => {
82+
it('returns null for meta-only key events and tab', () => {
83+
const metaOnlyKeys = ['Meta', 'Alt', 'Control', 'Shift', 'Tab'];
84+
85+
metaOnlyKeys.forEach((key) => {
86+
const event = { key } as KeyboardEventLike;
87+
expect(eventToShortcut(event)).toBe(null);
88+
});
89+
});
90+
91+
it('processes modifier keys correctly', () => {
92+
const event = {
93+
key: 'K',
94+
altKey: true,
95+
ctrlKey: true,
96+
metaKey: true,
97+
shiftKey: true,
98+
} as KeyboardEventLike;
99+
100+
expect(eventToShortcut(event)).toEqual(['alt', 'control', 'meta', 'shift', 'K']);
101+
});
102+
103+
it('handles single letter keys correctly', () => {
104+
const event = {
105+
key: 'k',
106+
} as KeyboardEventLike;
107+
108+
expect(eventToShortcut(event)).toEqual(['K']);
109+
});
110+
111+
it('handles space key correctly', () => {
112+
const event = {
113+
key: ' ',
114+
} as KeyboardEventLike;
115+
116+
expect(eventToShortcut(event)).toEqual(['space']);
117+
});
118+
119+
it('handles escape key correctly', () => {
120+
const event = {
121+
key: 'Escape',
122+
} as KeyboardEventLike;
123+
124+
expect(eventToShortcut(event)).toEqual(['escape']);
125+
});
126+
127+
it('handles arrow keys correctly', () => {
128+
const arrowKeys = ['ArrowRight', 'ArrowDown', 'ArrowUp', 'ArrowLeft'];
129+
130+
arrowKeys.forEach((key) => {
131+
const event = { key } as KeyboardEventLike;
132+
expect(eventToShortcut(event)).toEqual([key]);
133+
});
134+
});
135+
136+
it('supports different key/code combinations', () => {
137+
const event = {
138+
key: 'a',
139+
code: 'KeyA',
140+
} as KeyboardEventLike;
141+
142+
expect(eventToShortcut(event)).toEqual(['A']);
143+
144+
// When event.code produces a different value than event.key (e.g., with alt key on Mac)
145+
const altEvent = {
146+
key: 'å', // A special character
147+
code: 'KeyA',
148+
} as KeyboardEventLike;
149+
150+
expect(eventToShortcut(altEvent)).toEqual([['Å', 'A']]);
151+
});
152+
});
153+
154+
describe('shortcutMatchesShortcut', () => {
155+
it('returns false when either shortcut is null', () => {
156+
expect(shortcutMatchesShortcut(null as any, ['alt', 'K'])).toBe(false);
157+
expect(shortcutMatchesShortcut(['alt', 'K'], null as any)).toBe(false);
158+
});
159+
160+
it('handles shift/ shortcuts correctly', () => {
161+
expect(shortcutMatchesShortcut(['shift', '/'], ['/'])).toBe(true);
162+
});
163+
164+
it('compares shortcuts of different lengths correctly', () => {
165+
expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'K', 'L'])).toBe(false);
166+
expect(shortcutMatchesShortcut(['alt', 'K', 'L'], ['alt', 'K'])).toBe(false);
167+
});
168+
169+
it('compares shortcuts with same length correctly', () => {
170+
expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'K'])).toBe(true);
171+
expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'J'])).toBe(false);
172+
expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'K'])).toBe(true);
173+
expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'L'])).toBe(true);
174+
expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'M'])).toBe(false);
175+
});
176+
});
177+
178+
describe('eventMatchesShortcut', () => {
179+
it('matches keyboard event to shortcut correctly', () => {
180+
const event = {
181+
key: 'K',
182+
altKey: true,
183+
ctrlKey: false,
184+
metaKey: false,
185+
shiftKey: false,
186+
} as KeyboardEventLike;
187+
188+
expect(eventMatchesShortcut(event, ['alt', 'K'])).toBe(true);
189+
expect(eventMatchesShortcut(event, ['meta', 'K'])).toBe(false);
190+
});
191+
});
192+
193+
describe('keyToSymbol', () => {
194+
it('converts modifier keys to symbols', () => {
195+
// For Mac
196+
vi.mocked(isMacLike).mockReturnValue(true);
197+
198+
expect(keyToSymbol('alt')).toBe('⌥');
199+
expect(keyToSymbol('control')).toBe('⌃');
200+
expect(keyToSymbol('meta')).toBe('⌘');
201+
expect(keyToSymbol('shift')).toBe('⇧​');
202+
203+
// For non-Mac
204+
vi.mocked(isMacLike).mockReturnValue(false);
205+
206+
expect(keyToSymbol('alt')).toBe('alt');
207+
});
208+
209+
it('converts special keys to symbols', () => {
210+
expect(keyToSymbol('Enter')).toBe('');
211+
expect(keyToSymbol('Backspace')).toBe('');
212+
expect(keyToSymbol('Esc')).toBe('');
213+
expect(keyToSymbol('escape')).toBe('');
214+
expect(keyToSymbol(' ')).toBe('SPACE');
215+
expect(keyToSymbol('ArrowUp')).toBe('↑');
216+
expect(keyToSymbol('ArrowDown')).toBe('↓');
217+
expect(keyToSymbol('ArrowLeft')).toBe('←');
218+
expect(keyToSymbol('ArrowRight')).toBe('→');
219+
});
220+
221+
it('converts regular keys to uppercase', () => {
222+
expect(keyToSymbol('a')).toBe('A');
223+
expect(keyToSymbol('1')).toBe('1');
224+
});
225+
});
226+
227+
describe('shortcutToHumanString', () => {
228+
it('converts shortcut to human-readable string', () => {
229+
// For Mac
230+
vi.mocked(isMacLike).mockReturnValue(true);
231+
232+
expect(shortcutToHumanString(['alt', 'K'])).toBe('⌥ K');
233+
expect(shortcutToHumanString(['control', 'alt', 'shift', 'K'])).toBe('⌃ ⌥ ⇧​ K');
234+
expect(shortcutToHumanString(['meta', 'ArrowUp'])).toBe('⌘ ↑');
235+
236+
// For non-Mac
237+
vi.mocked(isMacLike).mockReturnValue(false);
238+
239+
expect(shortcutToHumanString(['alt', 'K'])).toBe('alt K');
240+
});
241+
});
242+
});

code/core/src/manager-api/lib/shortcut.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { global } from '@storybook/global';
22

33
import type { API_KeyCollection } from '../modules/shortcuts';
4+
import { isMacLike } from './platform';
45

5-
const { navigator } = global;
6-
7-
export const isMacLike = () =>
8-
navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false;
96
export const controlOrMetaSymbol = () => (isMacLike() ? '⌘' : 'ctrl');
107
export const controlOrMetaKey = () => (isMacLike() ? 'meta' : 'control');
118
export const optionOrAltSymbol = () => (isMacLike() ? '⌥' : 'alt');

code/core/src/manager-api/root.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import type { Options } from './store';
6666
import Store from './store';
6767

6868
export * from './lib/request-response';
69+
export * from './lib/platform';
6970
export * from './lib/shortcut';
7071

7172
const { ActiveTabs } = layout;

0 commit comments

Comments
 (0)