mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-08 13:59:03 +02:00
bdffafa5df
* refactor: Data-attribute constants + boolean string enum * refactor: Use CSS class string constants * refactor: Address review comments
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
// Guards the editing-key contract of the ChatFormInputRich:
|
|
// undo/redo is replayed from source snapshots (the token rebuilds destroy
|
|
// the native undo stack), and Tab is NOT intercepted (WCAG 2.1.2 no
|
|
// keyboard trap), matching the plain textarea.
|
|
|
|
import ChatFormInputRichHarness from './components/ChatFormInputRichHarness.svelte';
|
|
import { tick } from 'svelte';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { render } from 'vitest-browser-svelte';
|
|
|
|
const SOURCE = 'see [docs](file:///a/b)';
|
|
|
|
function editableIn(container: HTMLElement): HTMLElement {
|
|
const el = container.querySelector('[role="textbox"]');
|
|
|
|
if (!(el instanceof HTMLElement)) throw new Error('ChatFormInputRich not rendered');
|
|
|
|
return el;
|
|
}
|
|
|
|
function type(root: HTMLElement, text: string, inputType = 'insertText') {
|
|
root.appendChild(document.createTextNode(text));
|
|
root.dispatchEvent(new InputEvent('input', { bubbles: true, inputType }));
|
|
}
|
|
|
|
function keydown(root: HTMLElement, init: KeyboardEventInit) {
|
|
const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init });
|
|
|
|
root.dispatchEvent(event);
|
|
|
|
return event;
|
|
}
|
|
|
|
describe('ChatFormInputRich undo/redo', () => {
|
|
it('undoes and redoes an edit across a badge-containing buffer', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: SOURCE });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
|
|
type(root, ' more');
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe(`${SOURCE} more`);
|
|
|
|
const undoEvent = keydown(root, { ctrlKey: true, key: 'z' });
|
|
|
|
await tick();
|
|
expect(undoEvent.defaultPrevented).toBe(true);
|
|
expect(screen.component.getValue()).toBe(SOURCE);
|
|
|
|
const redoEvent = keydown(root, { ctrlKey: true, key: 'z', shiftKey: true });
|
|
|
|
await tick();
|
|
expect(redoEvent.defaultPrevented).toBe(true);
|
|
expect(screen.component.getValue()).toBe(`${SOURCE} more`);
|
|
});
|
|
|
|
it('redoes with Ctrl+Y as well', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: SOURCE });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
|
|
type(root, ' more');
|
|
await tick();
|
|
keydown(root, { key: 'z', metaKey: true });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe(SOURCE);
|
|
|
|
keydown(root, { ctrlKey: true, key: 'y' });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe(`${SOURCE} more`);
|
|
});
|
|
|
|
it('coalesces a typing burst into one undo step', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: 'abc' });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
|
|
type(root, 'd');
|
|
type(root, 'e');
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abcde');
|
|
|
|
keydown(root, { ctrlKey: true, key: 'z' });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abc');
|
|
});
|
|
|
|
it('keeps a newline as its own undo step', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: 'abc' });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
|
|
type(root, 'd');
|
|
type(root, '\n', 'insertLineBreak');
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abcd\n');
|
|
|
|
keydown(root, { ctrlKey: true, key: 'z' });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abcd');
|
|
|
|
keydown(root, { ctrlKey: true, key: 'z' });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abc');
|
|
});
|
|
|
|
it('is a no-op when there is nothing to undo', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: 'abc' });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
const event = keydown(root, { ctrlKey: true, key: 'z' });
|
|
|
|
await tick();
|
|
|
|
expect(event.defaultPrevented).toBe(true);
|
|
expect(screen.component.getValue()).toBe('abc');
|
|
});
|
|
|
|
it('abandons the redo branch after a fresh edit', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: 'abc' });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
|
|
type(root, 'd');
|
|
await tick();
|
|
keydown(root, { ctrlKey: true, key: 'z' });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abc');
|
|
|
|
type(root, 'e');
|
|
await tick();
|
|
keydown(root, { ctrlKey: true, key: 'z', shiftKey: true });
|
|
await tick();
|
|
expect(screen.component.getValue()).toBe('abce');
|
|
});
|
|
});
|
|
|
|
describe('ChatFormInputRich Tab key', () => {
|
|
it('does not trap Tab (focus can leave the editable)', async () => {
|
|
const screen = render(ChatFormInputRichHarness, { value: SOURCE });
|
|
|
|
await tick();
|
|
|
|
const root = editableIn(screen.container);
|
|
const event = keydown(root, { key: 'Tab' });
|
|
|
|
expect(event.defaultPrevented).toBe(false);
|
|
});
|
|
});
|