qwen-code/packages/web-shell/client/components/Editor.keymap.test.ts
易良 c1bc6b202e
feat(web-shell): add Option+Enter and Cmd+Enter newline shortcuts (#5005)
* feat(web-shell): add Option+Enter and Cmd+Enter newline shortcuts

The web terminal input only bound Shift+Enter and Ctrl+J for inserting a
newline. Option/Alt+Enter — common terminal muscle memory — did nothing,
and Cmd+Enter (bound in the CLI TUI's NEWLINE command) was missing too.

Add `Alt-Enter` and `Mod-Enter` bindings (Mod resolves to Cmd on macOS,
Ctrl elsewhere, mirroring the TUI's command/ctrl+return bindings), and
refactor the duplicated newline handler into a shared `insertNewline`.
The shortcuts panel label is now platform-aware (opt+enter on macOS,
alt+enter otherwise).

Adds a regression test that mounts a real CodeMirror EditorView and
dispatches real keydown events to lock the new key resolutions.

* fix(web-shell): address review — show Mod+Enter in shortcuts panel, tighten Mod keymap test

- ShortcutsPanel: the newline label omitted the Mod+Enter binding; now shows
  cmd+enter (macOS) / ctrl+enter (elsewhere) alongside opt/alt+enter so the
  panel matches the actual keymap.
- Editor.keymap.test: the Mod+Enter case dispatched both metaKey and ctrlKey,
  so the platform-mismatched press was a silent no-op and the assertion passed
  regardless. Dispatch only the platform-correct modifier and assert exactly
  '\n', so breaking Ctrl+Enter on non-mac (or Cmd+Enter on mac) now fails.
2026-06-12 13:07:56 +08:00

108 lines
3.4 KiB
TypeScript

// @vitest-environment jsdom
//
// Regression test for the Editor submit/newline keymap. Mounts a real
// CodeMirror EditorView with the same bindings as Editor.tsx and dispatches
// real keydown events, asserting that:
// - Enter submits (does not insert a newline)
// - Shift+Enter / Ctrl+J / Mod(Cmd/Ctrl)+Enter / Alt(Option)+Enter insert '\n'
// This locks the resolution of the 'Alt-Enter' and 'Mod-Enter' key strings so
// Option/Cmd+Enter newline input cannot silently regress.
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState, Prec } from '@codemirror/state';
function mountEditor() {
let submitted = false;
const insertNewline = (view: EditorView) => {
view.dispatch(view.state.replaceSelection('\n'));
return true;
};
const submitKeymap = keymap.of([
{
key: 'Enter',
run: () => {
submitted = true;
return true;
},
},
{ key: 'Shift-Enter', run: insertNewline },
{ key: 'Ctrl-j', run: insertNewline },
{ key: 'Mod-Enter', run: insertNewline },
{ key: 'Alt-Enter', run: insertNewline },
]);
const parent = document.createElement('div');
document.body.appendChild(parent);
const view = new EditorView({
state: EditorState.create({ extensions: [Prec.highest(submitKeymap)] }),
parent,
});
return {
view,
getSubmitted: () => submitted,
resetSubmitted: () => {
submitted = false;
},
cleanup: () => {
view.destroy();
parent.remove();
},
};
}
function press(
view: EditorView,
init: { key: string; code: string } & KeyboardEventInit,
) {
view.contentDOM.dispatchEvent(
new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }),
);
}
describe('Editor newline keymap (runtime)', () => {
let h: ReturnType<typeof mountEditor>;
beforeEach(() => {
h = mountEditor();
});
afterEach(() => {
h.cleanup();
});
it('Enter submits without inserting a newline', () => {
press(h.view, { key: 'Enter', code: 'Enter' });
expect(h.getSubmitted()).toBe(true);
expect(h.view.state.doc.toString()).toBe('');
});
it('Shift+Enter inserts a newline and does not submit', () => {
press(h.view, { key: 'Enter', code: 'Enter', shiftKey: true });
expect(h.getSubmitted()).toBe(false);
expect(h.view.state.doc.toString()).toBe('\n');
});
it('Ctrl+J inserts a newline and does not submit', () => {
press(h.view, { key: 'j', code: 'KeyJ', ctrlKey: true });
expect(h.getSubmitted()).toBe(false);
expect(h.view.state.doc.toString()).toBe('\n');
});
it('Alt/Option+Enter inserts a newline and does not submit', () => {
press(h.view, { key: 'Enter', code: 'Enter', altKey: true });
expect(h.getSubmitted()).toBe(false);
expect(h.view.state.doc.toString()).toBe('\n');
});
it('Mod+Enter (Cmd on mac / Ctrl elsewhere) inserts a newline', () => {
// CodeMirror's `Mod-` prefix resolves to exactly one modifier per platform
// (Meta on mac, Ctrl elsewhere). Dispatch the platform-correct one so the
// assertion fails if Ctrl+Enter is ever broken on non-mac (and vice versa).
const isMac = /Mac/i.test(navigator.platform ?? '');
press(h.view, {
key: 'Enter',
code: 'Enter',
metaKey: isMac,
ctrlKey: !isMac,
});
expect(h.getSubmitted()).toBe(false);
expect(h.view.state.doc.toString()).toBe('\n');
});
});