251 lines
9.2 KiB
TypeScript
251 lines
9.2 KiB
TypeScript
import TextBuffer from "../src/text-buffer";
|
||
import { describe, it, expect } from "vitest";
|
||
|
||
// The purpose of this test‑suite is NOT to make the implementation green today
|
||
// – quite the opposite. We capture behaviours that are already covered by the
|
||
// reference Rust implementation (textarea.rs) but are *still missing* from the
|
||
// current TypeScript port. Every test is therefore marked with `.fails()` so
|
||
// that the suite passes while the functionality is absent. When a particular
|
||
// gap is closed the corresponding test will begin to succeed, causing Vitest to
|
||
// raise an error (a *good* error) that reminds us to remove the `.fails` flag.
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Soft‑tab insertion */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("soft‑tab insertion (↹ => 4 spaces)", () => {
|
||
it.fails(
|
||
"inserts 4 spaces at caret position when hard‑tab mode is off",
|
||
() => {
|
||
const buf = new TextBuffer("");
|
||
|
||
// A literal "\t" character is treated as user pressing the Tab key. The
|
||
// Rust version expands it to soft‑tabs by default.
|
||
buf.insert("\t");
|
||
|
||
expect(buf.getText()).toBe(" ");
|
||
expect(buf.getCursor()).toEqual([0, 4]);
|
||
},
|
||
);
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Undo / Redo – grouping & stack clearing */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("undo / redo – advanced behaviour", () => {
|
||
it.fails(
|
||
"typing a word character‑by‑character should undo in one step",
|
||
() => {
|
||
const buf = new TextBuffer("");
|
||
|
||
for (const ch of "hello") {
|
||
buf.insert(ch);
|
||
}
|
||
|
||
// One single undo should revert the *whole* word, leaving empty buffer.
|
||
buf.undo();
|
||
|
||
expect(buf.getText()).toBe("");
|
||
expect(buf.getCursor()).toEqual([0, 0]);
|
||
},
|
||
);
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Selection – cut / delete selection */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("selection – cut/delete", () => {
|
||
it.fails(
|
||
"cut() removes the selected range and yanks it into clipboard",
|
||
() => {
|
||
const buf = new TextBuffer("foo bar baz");
|
||
|
||
// Select the middle word "bar"
|
||
buf.move("wordRight"); // after "foo" + space => col 4
|
||
buf.startSelection();
|
||
buf.move("wordRight"); // after "bar" (col 8)
|
||
// @ts-expect-error – method missing in current implementation
|
||
buf.cut();
|
||
|
||
// Text should now read "foo baz" (two spaces collapsed only if impl trims)
|
||
expect(buf.getText()).toBe("foo baz");
|
||
|
||
// Cursor should be at the start of the gap where text was removed
|
||
expect(buf.getCursor()).toEqual([0, 4]);
|
||
|
||
// And clipboard/yank buffer should contain the deleted word
|
||
// @ts-expect-error – clipboard getter not exposed yet
|
||
expect(buf.getClipboard()).toBe("bar");
|
||
},
|
||
);
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Word‑wise forward deletion (Ctrl+Delete) */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("delete_next_word (Ctrl+Delete)", () => {
|
||
it.fails("removes everything until the next word boundary", () => {
|
||
const vp = { width: 80, height: 25 };
|
||
const buf = new TextBuffer("hello world!! next");
|
||
|
||
// Place caret at start of line (0,0). One Ctrl+Delete should wipe the
|
||
// word "hello" and the following space.
|
||
buf.handleInput(undefined, { delete: true, ctrl: true }, vp);
|
||
|
||
expect(buf.getText()).toBe("world!! next");
|
||
expect(buf.getCursor()).toEqual([0, 0]);
|
||
});
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Configurable tab length */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("tab length configuration", () => {
|
||
it.fails("inserts the configured number of spaces when tabLen=2", () => {
|
||
// @ts-expect-error – constructor currently has no config object
|
||
const buf = new TextBuffer("", { tabLen: 2 });
|
||
|
||
buf.insert("\t");
|
||
|
||
expect(buf.getText()).toBe(" "); // two spaces
|
||
expect(buf.getCursor()).toEqual([0, 2]);
|
||
});
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Search subsystem */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("search / regex navigation", () => {
|
||
it.fails("search_forward jumps to the next match", () => {
|
||
const text = [
|
||
"alpha beta gamma",
|
||
"beta gamma alpha",
|
||
"gamma alpha beta",
|
||
].join("\n");
|
||
|
||
const buf = new TextBuffer(text);
|
||
|
||
// @ts-expect-error – method missing
|
||
buf.setSearchPattern(/beta/);
|
||
|
||
// Cursor starts at 0,0. First search_forward should land on the first
|
||
// occurrence (row 0, col 6)
|
||
// @ts-expect-error – method missing
|
||
buf.searchForward();
|
||
|
||
expect(buf.getCursor()).toEqual([0, 6]);
|
||
|
||
// Second invocation should wrap within viewport and find next occurrence
|
||
// (row 1, col 0)
|
||
// @ts-expect-error – method missing
|
||
buf.searchForward();
|
||
|
||
expect(buf.getCursor()).toEqual([1, 0]);
|
||
});
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Word‑wise navigation accuracy */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("wordLeft / wordRight – punctuation boundaries", () => {
|
||
it.fails("wordLeft stops after punctuation like hyphen (-)", () => {
|
||
const buf = new TextBuffer("hello-world");
|
||
|
||
// Place caret at end of line
|
||
buf.move("end");
|
||
|
||
// Perform a single wordLeft – in Rust implementation this lands right
|
||
// *after* the hyphen, i.e. between '-' and 'w' (column index 6).
|
||
buf.move("wordLeft");
|
||
|
||
expect(buf.getCursor()).toEqual([0, 6]);
|
||
});
|
||
|
||
it.fails(
|
||
"wordRight stops after punctuation like underscore (_) which is not in JS boundary set",
|
||
() => {
|
||
const buf = new TextBuffer("foo_bar");
|
||
|
||
// From start, one wordRight should land right after the underscore (col 4)
|
||
buf.move("wordRight");
|
||
|
||
expect(buf.getCursor()).toEqual([0, 4]);
|
||
},
|
||
);
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Word‑wise deletion (Ctrl+Backspace) */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("word deletion shortcuts", () => {
|
||
it.fails("Ctrl+Backspace deletes the previous word", () => {
|
||
const vp = { width: 80, height: 25 };
|
||
const buf = new TextBuffer("hello world");
|
||
|
||
// Place caret after the last character
|
||
buf.move("end");
|
||
|
||
// Simulate Ctrl+Backspace (terminal usually sends backspace with ctrl flag)
|
||
buf.handleInput(undefined, { backspace: true, ctrl: true }, vp);
|
||
|
||
// The whole word "world" (and the preceding space) should be removed,
|
||
// leaving just "hello".
|
||
expect(buf.getText()).toBe("hello");
|
||
expect(buf.getCursor()).toEqual([0, 5]);
|
||
});
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Paragraph navigation */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("paragraph navigation", () => {
|
||
it.fails("Jumping forward by paragraph stops after a blank line", () => {
|
||
const text = [
|
||
"first paragraph line 1",
|
||
"first paragraph line 2",
|
||
"", // blank line separates paragraphs
|
||
"second paragraph line 1",
|
||
].join("\n");
|
||
|
||
const buf = new TextBuffer(text);
|
||
|
||
// Start at very beginning
|
||
// (No method exposed yet – once implemented we will call move("paragraphForward"))
|
||
// For now we imitate the call; test will fail until the command exists.
|
||
// @ts-expect-error – method not implemented yet
|
||
buf.move("paragraphForward");
|
||
|
||
// Expect caret to land at start of the first line _after_ the blank one
|
||
expect(buf.getCursor()).toEqual([3, 0]);
|
||
});
|
||
});
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Independent scrolling */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
describe("viewport scrolling independent of cursor", () => {
|
||
it.fails("scrolls without moving the caret", () => {
|
||
const lines = Array.from({ length: 100 }, (_, i) => `line ${i}`);
|
||
const buf = new TextBuffer(lines.join("\n"));
|
||
const vp = { width: 10, height: 5 };
|
||
|
||
// Cursor stays at 0,0. We now ask the view to scroll down by one page.
|
||
// @ts-expect-error – method not implemented yet
|
||
buf.scroll("pageDown", vp);
|
||
|
||
// Cursor must remain at (0,0) even though viewport origin changed.
|
||
expect(buf.getCursor()).toEqual([0, 0]);
|
||
// The first visible line should now be "line 5".
|
||
expect(buf.getVisibleLines(vp)[0]).toBe("line 5");
|
||
});
|
||
});
|