Files
openchamber/packages/ui/src/lib/addSelectionToChat.test.ts
T
Cursor AgentandSerhii Dziupin 682c42df8f fix(ui): harden Ctrl+L selection capture and menu delivery
Cover CodeMirror and DOM capture paths in tests, collapse text-control
selections after capture, and deliver the desktop Edit-menu action over
a single IPC channel so append cannot double-fire.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-03 14:24:22 +00:00

299 lines
8.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
const focusChatInputCalls: number[] = [];
const pendingInputCalls: Array<{ text: string | null; mode?: string }> = [];
const activeMainTabCalls: string[] = [];
const sessionSwitcherCalls: boolean[] = [];
const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = [];
type MockCodeMirrorView = {
state: {
selection: { main: { from: number; to: number } };
sliceDoc: (from: number, to: number) => string;
};
dispatch: (transaction: { selection: { anchor: number } }) => void;
};
let codeMirrorView: MockCodeMirrorView | null = null;
mock.module('@codemirror/view', () => ({
EditorView: {
findFromDOM: () => codeMirrorView,
},
}));
mock.module('@/components/chat/composer/editor/dom', () => ({
focusChatInput: () => {
focusChatInputCalls.push(1);
},
}));
mock.module('@/sync/input-store', () => ({
useInputStore: {
getState: () => ({
setPendingInputText: (text: string | null, mode?: string) => {
pendingInputCalls.push({ text, mode });
},
}),
},
}));
mock.module('@/stores/useUIStore', () => ({
useUIStore: {
getState: () => ({
setActiveMainTab: (tab: string) => {
activeMainTabCalls.push(tab);
},
setSessionSwitcherOpen: (open: boolean) => {
sessionSwitcherCalls.push(open);
},
}),
},
}));
const { addSelectionToChat, captureSelectionMarkdownForChat } = await import('./addSelectionToChat');
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const installSelectionEnvironment = (options: {
activeElement?: Element | null;
focusedCodeMirror?: Element | null;
selection?: Selection | null;
} = {}) => {
const {
activeElement = null,
focusedCodeMirror = null,
selection = null,
} = options;
const documentLike = {
activeElement,
querySelector: (selector: string) => {
if (selector === '.cm-editor.cm-focused') {
return focusedCodeMirror;
}
return null;
},
};
const windowLike = {
getSelection: () => selection,
};
Object.defineProperty(globalThis, 'document', { value: documentLike, configurable: true });
Object.defineProperty(globalThis, 'window', { value: windowLike, configurable: true });
};
const clearCalls = () => {
focusChatInputCalls.length = 0;
pendingInputCalls.length = 0;
activeMainTabCalls.length = 0;
sessionSwitcherCalls.length = 0;
codeMirrorDispatches.length = 0;
codeMirrorView = null;
};
afterEach(() => {
Object.defineProperty(globalThis, 'document', { value: originalDocument, configurable: true });
Object.defineProperty(globalThis, 'window', { value: originalWindow, configurable: true });
});
describe('captureSelectionMarkdownForChat', () => {
beforeEach(() => {
clearCalls();
});
test('returns null when nothing is selected', () => {
installSelectionEnvironment();
expect(captureSelectionMarkdownForChat()).toBeNull();
});
test('captures a textarea selection outside the composer and collapses it', () => {
const textarea = {
tagName: 'TEXTAREA',
value: 'alpha beta gamma',
selectionStart: 6,
selectionEnd: 10,
closest: () => null,
} as unknown as HTMLTextAreaElement;
installSelectionEnvironment({ activeElement: textarea });
expect(captureSelectionMarkdownForChat()).toBe('```md\nbeta\n```');
expect(textarea.selectionStart).toBe(10);
expect(textarea.selectionEnd).toBe(10);
});
test('ignores selections inside the chat composer', () => {
const textarea = {
tagName: 'TEXTAREA',
value: 'draft text',
selectionStart: 0,
selectionEnd: 5,
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? textarea : null),
} as unknown as HTMLTextAreaElement;
installSelectionEnvironment({ activeElement: textarea });
expect(captureSelectionMarkdownForChat()).toBeNull();
});
test('captures a focused CodeMirror selection outside the composer and collapses it', () => {
const focusedEditor = {
closest: () => null,
} as unknown as HTMLElement;
codeMirrorView = {
state: {
selection: { main: { from: 4, to: 11 } },
sliceDoc: (from: number, to: number) => 'const x'.slice(0, to - from),
},
dispatch: (transaction) => {
codeMirrorDispatches.push(transaction);
codeMirrorView!.state.selection.main = {
from: transaction.selection.anchor,
to: transaction.selection.anchor,
};
},
};
// sliceDoc should return the selected slice; use explicit text instead of slice math.
codeMirrorView.state.sliceDoc = () => 'const x';
installSelectionEnvironment({ focusedCodeMirror: focusedEditor });
expect(captureSelectionMarkdownForChat()).toBe('```\nconst x\n```');
expect(codeMirrorDispatches).toEqual([{ selection: { anchor: 11 } }]);
expect(codeMirrorView.state.selection.main).toEqual({ from: 11, to: 11 });
});
test('ignores a focused CodeMirror editor inside the chat composer', () => {
const focusedEditor = {
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? focusedEditor : null),
} as unknown as HTMLElement;
codeMirrorView = {
state: {
selection: { main: { from: 0, to: 5 } },
sliceDoc: () => 'draft',
},
dispatch: (transaction) => {
codeMirrorDispatches.push(transaction);
},
};
installSelectionEnvironment({ focusedCodeMirror: focusedEditor });
expect(captureSelectionMarkdownForChat()).toBeNull();
expect(codeMirrorDispatches).toEqual([]);
});
test('captures a DOM selection from chat-message content and clears it', () => {
const parent = {
closest: (selector: string) => (selector === 'pre code' ? null : null),
};
const textNode = {
nodeType: 3,
parentElement: parent,
};
let rangeCount = 1;
let collapsed = false;
const selection = {
get rangeCount() {
return rangeCount;
},
get isCollapsed() {
return collapsed;
},
toString: () => 'Hello world',
getRangeAt: () => ({
commonAncestorContainer: textNode,
startContainer: textNode,
endContainer: textNode,
cloneContents: () => ({ childNodes: [] }),
}),
removeAllRanges: () => {
rangeCount = 0;
collapsed = true;
},
} as unknown as Selection;
installSelectionEnvironment({ selection });
expect(captureSelectionMarkdownForChat()).toBe('```md\nHello world\n```');
expect(selection.rangeCount).toBe(0);
expect(selection.isCollapsed).toBe(true);
});
test('ignores a DOM selection inside the chat composer', () => {
const composerHost = {};
const parent = {
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? composerHost : null),
};
const textNode = {
nodeType: 3,
parentElement: parent,
};
const selection = {
rangeCount: 1,
isCollapsed: false,
toString: () => 'draft',
getRangeAt: () => ({
commonAncestorContainer: textNode,
startContainer: textNode,
endContainer: textNode,
cloneContents: () => ({ childNodes: [] }),
}),
removeAllRanges: () => undefined,
} as unknown as Selection;
installSelectionEnvironment({ selection });
expect(captureSelectionMarkdownForChat()).toBeNull();
});
});
describe('addSelectionToChat', () => {
beforeEach(() => {
clearCalls();
});
test('appends captured selection and focuses chat input', async () => {
const textarea = {
tagName: 'TEXTAREA',
value: 'selected',
selectionStart: 0,
selectionEnd: 8,
closest: () => null,
} as unknown as HTMLTextAreaElement;
installSelectionEnvironment({ activeElement: textarea });
expect(addSelectionToChat()).toBe(true);
expect(activeMainTabCalls).toEqual(['chat']);
expect(sessionSwitcherCalls).toEqual([false]);
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
await Promise.resolve();
expect(focusChatInputCalls.length).toBe(1);
});
test('second capture after textarea collapse does not append again', () => {
const textarea = {
tagName: 'TEXTAREA',
value: 'selected',
selectionStart: 0,
selectionEnd: 8,
closest: () => null,
} as unknown as HTMLTextAreaElement;
installSelectionEnvironment({ activeElement: textarea });
expect(addSelectionToChat()).toBe(true);
expect(addSelectionToChat()).toBe(false);
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
});
test('focuses chat input when nothing is selected', async () => {
installSelectionEnvironment();
expect(addSelectionToChat()).toBe(false);
expect(pendingInputCalls).toEqual([]);
expect(activeMainTabCalls).toEqual(['chat']);
await Promise.resolve();
expect(focusChatInputCalls.length).toBe(1);
});
});