fix(command-palette): guard keydown with undefined key (no crash)
Some keydown events (autofill, IME/composition, certain extensions) arrive with e.key === undefined, crashing the hotkey handler at e.key.toLowerCase(). Guard e.key (and hotkey.key) before comparing. +1 test (undefined-key keydown is ignored without throwing). 27 pass. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
60989d7b62
commit
de7e0dcb84
@ -85,7 +85,7 @@ describe('CommandRegistryProvider', () => {
|
||||
]);
|
||||
return useCommands();
|
||||
},
|
||||
{ wrapper: wrapper() },
|
||||
{ wrapper: wrapper() }
|
||||
);
|
||||
expect(result.current.map(c => c.id).sort()).toEqual(['temp', 'temp2']);
|
||||
unmount();
|
||||
@ -96,9 +96,7 @@ describe('CommandRegistryProvider', () => {
|
||||
});
|
||||
|
||||
it('throws when used outside provider', () => {
|
||||
expect(() => renderHook(() => useCommands())).toThrow(
|
||||
/CommandRegistryProvider/,
|
||||
);
|
||||
expect(() => renderHook(() => useCommands())).toThrow(/CommandRegistryProvider/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,7 +126,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open={false} onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
expect(screen.queryByTestId('bl-cmdk')).toBeNull();
|
||||
});
|
||||
@ -137,7 +135,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
expect(screen.getByTestId('bl-cmdk-panel')).toBeDefined();
|
||||
expect(screen.getByTestId('bl-cmdk-item-new-task')).toBeDefined();
|
||||
@ -149,7 +147,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
expect(screen.queryByTestId('bl-cmdk-item-gated')).toBeNull();
|
||||
});
|
||||
@ -158,7 +156,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
const dialog = screen.getByTestId('bl-cmdk');
|
||||
fireEvent.keyDown(dialog, { key: 'Tab' });
|
||||
@ -171,7 +169,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId('bl-cmdk-input'), {
|
||||
target: { value: 'task' },
|
||||
@ -184,10 +182,10 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={onClose} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Enter' });
|
||||
expect((seed[0].run as ReturnType<typeof vi.fn>)).toHaveBeenCalledOnce();
|
||||
expect(seed[0].run as ReturnType<typeof vi.fn>).toHaveBeenCalledOnce();
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -197,7 +195,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={onClose} onNavigate={onNavigate} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Tab' });
|
||||
fireEvent.keyDown(screen.getByTestId('bl-cmdk'), { key: 'Enter' });
|
||||
@ -209,7 +207,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
const dialog = screen.getByTestId('bl-cmdk');
|
||||
fireEvent.keyDown(dialog, { key: 'ArrowDown' });
|
||||
@ -225,7 +223,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={onClose} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
@ -235,7 +233,7 @@ describe('CommandPalette', () => {
|
||||
const { rerender } = render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} initialMode="ask-ai" />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
expect(screen.getByTestId('bl-cmdk-ask-ai')).toBeDefined();
|
||||
|
||||
@ -247,7 +245,7 @@ describe('CommandPalette', () => {
|
||||
initialMode="ask-ai"
|
||||
askAiPanel={q => <div data-testid="custom-ai">{q || 'idle'}</div>}
|
||||
/>
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
expect(screen.getByTestId('custom-ai').textContent).toBe('idle');
|
||||
});
|
||||
@ -256,12 +254,12 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
const row = screen.getByTestId('bl-cmdk-item-archive');
|
||||
expect(row.getAttribute('aria-disabled')).toBe('true');
|
||||
fireEvent.click(row);
|
||||
expect((seed[3].run as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
|
||||
expect(seed[3].run as ReturnType<typeof vi.fn>).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('recents persist to localStorage when a command is run', () => {
|
||||
@ -294,7 +292,7 @@ describe('CommandPalette', () => {
|
||||
render(
|
||||
<CommandRegistryProvider initial={seed}>
|
||||
<CommandPalette open onClose={() => {}} />
|
||||
</CommandRegistryProvider>,
|
||||
</CommandRegistryProvider>
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('bl-cmdk-item-new-task'));
|
||||
expect(mem['bl-cmdk-recents']).toContain('new-task');
|
||||
@ -315,16 +313,12 @@ describe('useCommandPalette', () => {
|
||||
expect(result.current.open).toBe(false);
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'k', metaKey: true }),
|
||||
);
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }));
|
||||
});
|
||||
expect(result.current.open).toBe(true);
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'k', metaKey: true }),
|
||||
);
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }));
|
||||
});
|
||||
expect(result.current.open).toBe(false);
|
||||
|
||||
@ -335,6 +329,19 @@ describe('useCommandPalette', () => {
|
||||
expect(result.current.open).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores keydown events with an undefined key (autofill/IME) without throwing', () => {
|
||||
const { result } = renderHook(() => useCommandPalette());
|
||||
expect(() =>
|
||||
act(() => {
|
||||
// Some events (autofill, IME) dispatch keydown with no `key` set.
|
||||
const ev = new KeyboardEvent('keydown', { metaKey: true });
|
||||
Object.defineProperty(ev, 'key', { value: undefined });
|
||||
window.dispatchEvent(ev);
|
||||
})
|
||||
).not.toThrow();
|
||||
expect(result.current.open).toBe(false);
|
||||
});
|
||||
|
||||
it('show/hide/toggle imperative helpers', () => {
|
||||
const { result } = renderHook(() => useCommandPalette({ hotkey: null }));
|
||||
act(() => result.current.show());
|
||||
@ -348,9 +355,7 @@ describe('useCommandPalette', () => {
|
||||
});
|
||||
|
||||
it('respects defaultOpen', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCommandPalette({ defaultOpen: true, hotkey: null }),
|
||||
);
|
||||
const { result } = renderHook(() => useCommandPalette({ defaultOpen: true, hotkey: null }));
|
||||
expect(result.current.open).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -32,10 +32,9 @@ export interface UseCommandPaletteHelpers {
|
||||
* inputs unless the user explicitly holds the modifier.
|
||||
*/
|
||||
export function useCommandPalette(
|
||||
options: UseCommandPaletteOptions = {},
|
||||
options: UseCommandPaletteOptions = {}
|
||||
): UseCommandPaletteHelpers {
|
||||
const { hotkey = { key: 'k', meta: true, ctrl: true }, defaultOpen = false } =
|
||||
options;
|
||||
const { hotkey = { key: 'k', meta: true, ctrl: true }, defaultOpen = false } = options;
|
||||
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const show = useCallback(() => setOpen(true), []);
|
||||
@ -45,6 +44,9 @@ export function useCommandPalette(
|
||||
useEffect(() => {
|
||||
if (!hotkey) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
// Some keydown events (autofill, IME/composition, certain extensions) arrive
|
||||
// with an undefined `key`; guard so we never crash on `.toLowerCase()`.
|
||||
if (!e.key || !hotkey.key) return;
|
||||
if (e.key.toLowerCase() !== hotkey.key.toLowerCase()) return;
|
||||
const wantMeta = hotkey.meta ?? false;
|
||||
const wantCtrl = hotkey.ctrl ?? false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user