Files
openchamber/packages/ui/src/components/chat/message/selectionMarkdown.test.ts
T
ChangeHow 8801d69c66 fix(ui): preserve Markdown code selections (#2449)
* fix(ui): preserve markdown code selections

* test(ui): clarify markdown selection fixtures

* fix(ui): preserve highlighted code languages

* fix(ui): preserve markdown block boundaries
2026-07-27 13:07:25 +03:00

184 lines
4.9 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import {
formatCodeSelectionMarkdown,
selectionNodesToMarkdown,
trimSelectionValue,
wrapMarkdownSelectionForChat,
} from './selectionMarkdown';
type TestNode =
| { type: 'text'; value: string }
| {
type: 'element';
tag: string;
className: string;
href: string;
component: string;
markdownLanguage: string;
isMarkdownBlock: boolean;
isCodeLines: boolean;
isCodeLineNumber: boolean;
children: TestNode[];
};
const multiline = (...lines: string[]): string => lines.join('\n');
const text = (value: string): TestNode => ({ type: 'text', value });
const element = (
tag: string,
children: TestNode[],
options: Partial<Omit<Extract<TestNode, { type: 'element' }>, 'type' | 'tag' | 'children'>> = {},
): TestNode => ({
type: 'element',
tag,
className: '',
href: '',
component: '',
markdownLanguage: '',
isMarkdownBlock: false,
isCodeLines: false,
isCodeLineNumber: false,
children,
...options,
});
const codeLine = (number: number, content: string): TestNode => element('span', [
element('span', [text(String(number))], { isCodeLineNumber: true }),
element('span', [text(content)]),
]);
const markdownBlock = (children: TestNode[]): TestNode => element('div', children, { isMarkdownBlock: true });
const codeWrapper = (lines: string[], language = 'ts'): TestNode => element('div', [
element('div', [text(language)]),
element('div', [
element('pre', [
element('code', lines.flatMap((line, index) => [
codeLine(index + 12, line),
...(index < lines.length - 1 ? [element('span', [text('\n')])] : []),
]), { isCodeLines: true }),
], { markdownLanguage: language }),
]),
], { component: 'markdown-code' });
describe('selectionNodesToMarkdown', () => {
test('serializes a complete grid code block without its header or line numbers', () => {
expect(selectionNodesToMarkdown([codeWrapper(['range.cloneContents()', 'next()'])], '')).toBe(multiline(
'```ts',
'range.cloneContents()',
'next()',
'```',
));
});
test('preserves a code block nested between production Markdown block wrappers', () => {
const nodes = [
markdownBlock([element('p', [
text('Method:'),
element('code', [text('Selection.toString()')]),
text('. Add to Chat uses a cloned range.'),
])]),
markdownBlock([codeWrapper(['range.cloneContents()'])]),
markdownBlock([element('p', [text('Following explanation')])]),
];
expect(selectionNodesToMarkdown(nodes, '')).toBe(multiline(
'Method:`Selection.toString()`. Add to Chat uses a cloned range.',
'',
'```ts',
'range.cloneContents()',
'```',
'',
'Following explanation',
));
});
test('preserves a partial code block selected before prose', () => {
expect(selectionNodesToMarkdown([
codeWrapper(['range.cloneContents()']),
element('p', [text('Following explanation')]),
], '')).toBe(multiline(
'```ts',
'range.cloneContents()',
'```',
'',
'Following explanation',
));
});
});
describe('formatCodeSelectionMarkdown', () => {
test('preserves indentation and blank lines', () => {
expect(formatCodeSelectionMarkdown(multiline(
'if (ready) {',
' run();',
'',
' stop();',
'}',
), 'ts')).toBe(multiline(
'```ts',
'if (ready) {',
' run();',
'',
' stop();',
'}',
'```',
));
});
test('normalizes line endings without duplicating a trailing newline', () => {
expect(formatCodeSelectionMarkdown('first\r\nsecond\r\n', 'text')).toBe(multiline(
'```text',
'first',
'second',
'```',
));
});
test('uses a longer fence when selected code contains backtick fences', () => {
expect(formatCodeSelectionMarkdown(multiline(
'before',
'```',
'after',
), 'md')).toBe(multiline(
'````md',
'before',
'```',
'after',
'````',
));
});
test('preserves punctuation in language identifiers', () => {
expect(selectionNodesToMarkdown([codeWrapper(['std::vector<int> values;'], 'c++')], '')).toBe(multiline(
'```c++',
'std::vector<int> values;',
'```',
));
});
});
describe('trimSelectionValue', () => {
test('normalizes line endings before trimming the selection', () => {
expect(trimSelectionValue(' first\r\nsecond ')).toBe(multiline('first', 'second'));
});
});
describe('wrapMarkdownSelectionForChat', () => {
test('uses a longer outer fence when the selection contains fenced code', () => {
const selectedMarkdown = multiline(
'```ts',
'run();',
'```',
);
expect(wrapMarkdownSelectionForChat(selectedMarkdown)).toBe(multiline(
'````md',
'```ts',
'run();',
'```',
'````',
));
});
});