import assert from "node:assert"; import { describe, it } from "node:test"; import { Editor } from "../src/components/editor.js"; import { defaultEditorTheme } from "./test-themes.js"; describe("Editor component", () => { describe("Prompt history navigation", () => { it("does nothing on Up arrow when history is empty", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("\x1b[A"); // Up arrow assert.strictEqual(editor.getText(), ""); }); it("shows most recent history entry on Up arrow when editor is empty", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("first prompt"); editor.addToHistory("second prompt"); editor.handleInput("\x1b[A"); // Up arrow assert.strictEqual(editor.getText(), "second prompt"); }); it("cycles through history entries on repeated Up arrow", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("first"); editor.addToHistory("second"); editor.addToHistory("third"); editor.handleInput("\x1b[A"); // Up - shows "third" assert.strictEqual(editor.getText(), "third"); editor.handleInput("\x1b[A"); // Up - shows "second" assert.strictEqual(editor.getText(), "second"); editor.handleInput("\x1b[A"); // Up - shows "first" assert.strictEqual(editor.getText(), "first"); editor.handleInput("\x1b[A"); // Up - stays at "first" (oldest) assert.strictEqual(editor.getText(), "first"); }); it("returns to empty editor on Down arrow after browsing history", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("prompt"); editor.handleInput("\x1b[A"); // Up - shows "prompt" assert.strictEqual(editor.getText(), "prompt"); editor.handleInput("\x1b[B"); // Down - clears editor assert.strictEqual(editor.getText(), ""); }); it("navigates forward through history with Down arrow", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("first"); editor.addToHistory("second"); editor.addToHistory("third"); // Go to oldest editor.handleInput("\x1b[A"); // third editor.handleInput("\x1b[A"); // second editor.handleInput("\x1b[A"); // first // Navigate back editor.handleInput("\x1b[B"); // second assert.strictEqual(editor.getText(), "second"); editor.handleInput("\x1b[B"); // third assert.strictEqual(editor.getText(), "third"); editor.handleInput("\x1b[B"); // empty assert.strictEqual(editor.getText(), ""); }); it("exits history mode when typing a character", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("old prompt"); editor.handleInput("\x1b[A"); // Up - shows "old prompt" editor.handleInput("x"); // Type a character - exits history mode assert.strictEqual(editor.getText(), "old promptx"); }); it("exits history mode on setText", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("first"); editor.addToHistory("second"); editor.handleInput("\x1b[A"); // Up - shows "second" editor.setText(""); // External clear // Up should start fresh from most recent editor.handleInput("\x1b[A"); assert.strictEqual(editor.getText(), "second"); }); it("does not add empty strings to history", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory(""); editor.addToHistory(" "); editor.addToHistory("valid"); editor.handleInput("\x1b[A"); assert.strictEqual(editor.getText(), "valid"); // Should not have more entries editor.handleInput("\x1b[A"); assert.strictEqual(editor.getText(), "valid"); }); it("does not add consecutive duplicates to history", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("same"); editor.addToHistory("same"); editor.addToHistory("same"); editor.handleInput("\x1b[A"); // "same" assert.strictEqual(editor.getText(), "same"); editor.handleInput("\x1b[A"); // stays at "same" (only one entry) assert.strictEqual(editor.getText(), "same"); }); it("allows non-consecutive duplicates in history", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("first"); editor.addToHistory("second"); editor.addToHistory("first"); // Not consecutive, should be added editor.handleInput("\x1b[A"); // "first" assert.strictEqual(editor.getText(), "first"); editor.handleInput("\x1b[A"); // "second" assert.strictEqual(editor.getText(), "second"); editor.handleInput("\x1b[A"); // "first" (older one) assert.strictEqual(editor.getText(), "first"); }); it("uses cursor movement instead of history when editor has content", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("history item"); editor.setText("line1\nline2"); // Cursor is at end of line2, Up should move to line1 editor.handleInput("\x1b[A"); // Up - cursor movement // Insert character to verify cursor position editor.handleInput("X"); // X should be inserted in line1, not replace with history assert.strictEqual(editor.getText(), "line1X\nline2"); }); it("limits history to 100 entries", () => { const editor = new Editor(defaultEditorTheme); // Add 105 entries for (let i = 0; i < 105; i++) { editor.addToHistory(`prompt ${i}`); } // Navigate to oldest for (let i = 0; i < 100; i++) { editor.handleInput("\x1b[A"); } // Should be at entry 5 (oldest kept), not entry 0 assert.strictEqual(editor.getText(), "prompt 5"); // One more Up should not change anything editor.handleInput("\x1b[A"); assert.strictEqual(editor.getText(), "prompt 5"); }); it("allows cursor movement within multi-line history entry with Down", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("line1\nline2\nline3"); // Browse to the multi-line entry editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end of line3 assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Down should exit history since cursor is on last line editor.handleInput("\x1b[B"); // Down assert.strictEqual(editor.getText(), ""); // Exited to empty }); it("allows cursor movement within multi-line history entry with Up", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("older entry"); editor.addToHistory("line1\nline2\nline3"); // Browse to the multi-line entry editor.handleInput("\x1b[A"); // Up - shows multi-line, cursor at end of line3 // Up should move cursor within the entry (not on first line yet) editor.handleInput("\x1b[A"); // Up - cursor moves to line2 assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry editor.handleInput("\x1b[A"); // Up - cursor moves to line1 (now on first visual line) assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Still same entry // Now Up should navigate to older history entry editor.handleInput("\x1b[A"); // Up - navigate to older assert.strictEqual(editor.getText(), "older entry"); }); it("navigates from multi-line entry back to newer via Down after cursor movement", () => { const editor = new Editor(defaultEditorTheme); editor.addToHistory("line1\nline2\nline3"); // Browse to entry and move cursor up editor.handleInput("\x1b[A"); // Up - shows entry, cursor at end editor.handleInput("\x1b[A"); // Up - cursor to line2 editor.handleInput("\x1b[A"); // Up - cursor to line1 // Now Down should move cursor down within the entry editor.handleInput("\x1b[B"); // Down - cursor to line2 assert.strictEqual(editor.getText(), "line1\nline2\nline3"); editor.handleInput("\x1b[B"); // Down - cursor to line3 assert.strictEqual(editor.getText(), "line1\nline2\nline3"); // Now on last line, Down should exit history editor.handleInput("\x1b[B"); // Down - exit to empty assert.strictEqual(editor.getText(), ""); }); }); describe("Unicode text editing behavior", () => { it("inserts mixed ASCII, umlauts, and emojis as literal text", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("H"); editor.handleInput("e"); editor.handleInput("l"); editor.handleInput("l"); editor.handleInput("o"); editor.handleInput(" "); editor.handleInput("ä"); editor.handleInput("ö"); editor.handleInput("ü"); editor.handleInput(" "); editor.handleInput("😀"); const text = editor.getText(); assert.strictEqual(text, "Hello äöü 😀"); }); it("deletes single-code-unit unicode characters (umlauts) with Backspace", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("ä"); editor.handleInput("ö"); editor.handleInput("ü"); // Delete the last character (ü) editor.handleInput("\x7f"); // Backspace const text = editor.getText(); assert.strictEqual(text, "äö"); }); it("deletes multi-code-unit emojis with repeated Backspace", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("😀"); editor.handleInput("👍"); // Delete the last emoji (👍) - requires 2 backspaces since emojis are 2 code units editor.handleInput("\x7f"); // Backspace editor.handleInput("\x7f"); // Backspace const text = editor.getText(); assert.strictEqual(text, "😀"); }); it("inserts characters at the correct position after cursor movement over umlauts", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("ä"); editor.handleInput("ö"); editor.handleInput("ü"); // Move cursor left twice editor.handleInput("\x1b[D"); // Left arrow editor.handleInput("\x1b[D"); // Left arrow // Insert 'x' in the middle editor.handleInput("x"); const text = editor.getText(); assert.strictEqual(text, "äxöü"); }); it("moves cursor in code units across multi-code-unit emojis before insertion", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("😀"); editor.handleInput("👍"); editor.handleInput("🎉"); // Move cursor left over last emoji (🎉) editor.handleInput("\x1b[D"); // Left arrow editor.handleInput("\x1b[D"); // Left arrow // Move cursor left over second emoji (👍) editor.handleInput("\x1b[D"); editor.handleInput("\x1b[D"); // Insert 'x' between first and second emoji editor.handleInput("x"); const text = editor.getText(); assert.strictEqual(text, "😀x👍🎉"); }); it("preserves umlauts across line breaks", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("ä"); editor.handleInput("ö"); editor.handleInput("ü"); editor.handleInput("\n"); // new line editor.handleInput("Ä"); editor.handleInput("Ö"); editor.handleInput("Ü"); const text = editor.getText(); assert.strictEqual(text, "äöü\nÄÖÜ"); }); it("replaces the entire document with unicode text via setText (paste simulation)", () => { const editor = new Editor(defaultEditorTheme); // Simulate bracketed paste / programmatic replacement editor.setText("Hällö Wörld! 😀 äöüÄÖÜß"); const text = editor.getText(); assert.strictEqual(text, "Hällö Wörld! 😀 äöüÄÖÜß"); }); it("moves cursor to document start on Ctrl+A and inserts at the beginning", () => { const editor = new Editor(defaultEditorTheme); editor.handleInput("a"); editor.handleInput("b"); editor.handleInput("\x01"); // Ctrl+A (move to start) editor.handleInput("x"); // Insert at start const text = editor.getText(); assert.strictEqual(text, "xab"); }); }); });