mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
696 lines
21 KiB
TypeScript
696 lines
21 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { GhostTextAddon } from "./autocomplete/GhostTextAddon.ts";
|
|
|
|
type RenderListener = () => void;
|
|
type ResizeListener = () => void;
|
|
|
|
class FakeElement {
|
|
public readonly style: Record<string, string> = {};
|
|
public textContent = "";
|
|
public className = "";
|
|
public children: FakeElement[] = [];
|
|
|
|
appendChild(child: FakeElement): FakeElement {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
|
|
insertBefore(child: FakeElement, referenceNode: FakeElement | null): FakeElement {
|
|
if (!referenceNode) {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
const index = this.children.indexOf(referenceNode);
|
|
if (index < 0) {
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
this.children.splice(index, 0, child);
|
|
return child;
|
|
}
|
|
|
|
remove(): void {
|
|
// No-op for tests.
|
|
}
|
|
|
|
querySelector(selector: string): FakeElement | null {
|
|
if (selector === ".xterm-screen") {
|
|
return this.children.find((child) => child.className === "xterm-screen") ?? null;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function installFakeDocument(): () => void {
|
|
const previousDocument = globalThis.document;
|
|
const fakeDocument = {
|
|
createElement() {
|
|
return new FakeElement();
|
|
},
|
|
} as unknown as Document;
|
|
Object.defineProperty(globalThis, "document", {
|
|
configurable: true,
|
|
value: fakeDocument,
|
|
});
|
|
return () => {
|
|
if (previousDocument === undefined) {
|
|
delete (globalThis as { document?: Document }).document;
|
|
return;
|
|
}
|
|
Object.defineProperty(globalThis, "document", {
|
|
configurable: true,
|
|
value: previousDocument,
|
|
});
|
|
};
|
|
}
|
|
|
|
function createFakeTerm() {
|
|
const renderListeners: RenderListener[] = [];
|
|
const resizeListeners: ResizeListener[] = [];
|
|
const element = new FakeElement();
|
|
const screen = new FakeElement();
|
|
screen.className = "xterm-screen";
|
|
element.appendChild(screen);
|
|
|
|
const term = {
|
|
element,
|
|
cols: 80,
|
|
rows: 24,
|
|
options: {
|
|
fontSize: 14,
|
|
fontFamily: "monospace",
|
|
},
|
|
buffer: {
|
|
active: {
|
|
cursorX: 2,
|
|
cursorY: 0,
|
|
},
|
|
},
|
|
_core: {
|
|
_renderService: {
|
|
dimensions: {
|
|
css: {
|
|
cell: {
|
|
width: 9,
|
|
height: 18,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
onRender(listener: RenderListener) {
|
|
renderListeners.push(listener);
|
|
return {
|
|
dispose() {
|
|
const index = renderListeners.indexOf(listener);
|
|
if (index >= 0) renderListeners.splice(index, 1);
|
|
},
|
|
};
|
|
},
|
|
onResize(listener: ResizeListener) {
|
|
resizeListeners.push(listener);
|
|
return {
|
|
dispose() {
|
|
const index = resizeListeners.indexOf(listener);
|
|
if (index >= 0) resizeListeners.splice(index, 1);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
return {
|
|
term,
|
|
ghostElement: () => screen.children[0]?.children[0] ?? null,
|
|
fireRender() {
|
|
for (const listener of [...renderListeners]) listener();
|
|
},
|
|
};
|
|
}
|
|
|
|
test("shifts ghost to predicted cursor column as matching input is typed", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.display, "block");
|
|
assert.equal(ghost.textContent, "cker");
|
|
// show() anchored at cursorX=2, cell width=9 → left=18.
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
addon.adjustToInput("doc");
|
|
|
|
// After one matching char, the ghost predicts the cursor has moved
|
|
// to column 3 and trims "c" from the tail so the next char starts
|
|
// where the echo will land. Not waiting for xterm's render keeps
|
|
// ghost + real input aligned across SSH echo latency.
|
|
assert.equal(ghost.style.display, "block");
|
|
assert.equal(ghost.textContent, "ker");
|
|
assert.equal(ghost.style.left, "27px");
|
|
assert.equal(addon.getGhostText(), "ker");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("walks the anchor column backwards on backspace so the ghost re-aligns", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
|
|
addon.adjustToInput("doc");
|
|
assert.equal(ghost.textContent, "ker");
|
|
assert.equal(ghost.style.left, "27px");
|
|
|
|
// Backspace below the anchor input — the ghost should shift *left*,
|
|
// not stay pinned at the show-time anchor column. Pinning would
|
|
// leave a visual gap between the real cursor and the ghost.
|
|
addon.adjustToInput("d");
|
|
assert.equal(ghost.textContent, "ocker");
|
|
// anchor was cursorX=2 captured at show(); "d" is 1 char below
|
|
// anchorInputLength=2 → predicted cursor column = 1.
|
|
assert.equal(ghost.style.left, "9px");
|
|
|
|
// Backspace past the anchor back to empty: left is clamped at 0.
|
|
addon.adjustToInput("");
|
|
assert.equal(ghost.textContent, "docker");
|
|
assert.equal(ghost.style.left, "0px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("advances the anchor by two cells when a CJK glyph is typed", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// Suggestion starts with a CJK char so the prefix-match survives
|
|
// the next keystroke.
|
|
addon.show("你好世界", "");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
// show() anchored at cursorX=2. Input length 0 → delta 0 → left=18.
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
addon.adjustToInput("你");
|
|
|
|
// One CJK char = 2 cells. Predicted col = 2 + 2 = 4 → left 36px.
|
|
assert.equal(ghost.textContent, "好世界");
|
|
assert.equal(ghost.style.left, "36px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("wraps the ghost to the next row when the predicted column crosses cols", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
// Shrink the terminal to 10 cols to keep the math obvious. Anchor at
|
|
// col 8 with 5 ASCII chars to type → predicted col = 13, which should
|
|
// wrap to col 3 of row 1.
|
|
term.cols = 10;
|
|
term.buffer.active.cursorX = 8;
|
|
addon.activate(term as never);
|
|
addon.show("abcdefghij", "ab");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.top, "0px");
|
|
|
|
addon.adjustToInput("abcde");
|
|
|
|
// Predicted col = 8 + (5-2) = 11 → wraps to col 1 on next row.
|
|
// cellWidth=9, cellHeight=18.
|
|
assert.equal(ghost.textContent, "fghij");
|
|
assert.equal(ghost.style.left, "9px");
|
|
assert.equal(ghost.style.top, "18px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("self-heals a stale anchor on render while no adjustToInput has fired", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// show() captures cursorX=2 — simulate this firing during the
|
|
// keystroke→echo gap by later advancing the live cursor and
|
|
// verifying the ghost anchor snaps to the echoed position.
|
|
addon.show("docker", "do");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.left, "18px");
|
|
|
|
term.buffer.active.cursorX = 5;
|
|
fireRender();
|
|
|
|
// Input hasn't moved from the show-time baseline, so updatePosition
|
|
// re-reads live cursor: new left = 5 * 9 = 45px.
|
|
assert.equal(ghost.style.left, "45px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("wraps the ghost to the previous row when deletion crosses a row boundary", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
term.cols = 10;
|
|
term.buffer.active.cursorX = 1;
|
|
term.buffer.active.cursorY = 1;
|
|
addon.activate(term as never);
|
|
// Anchored at row 1 col 1 with 5 chars already typed.
|
|
addon.show("abcdefghij", "abcde");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
|
|
// Backspace back to 2 chars — delta = -3 across a row boundary.
|
|
addon.adjustToInput("ab");
|
|
|
|
// targetCol = 1 - 3 = -2 → col = 8 (wrapped) on row 0.
|
|
assert.equal(ghost.textContent, "cdefghij");
|
|
assert.equal(ghost.style.left, "72px");
|
|
assert.equal(ghost.style.top, "0px");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides ghost immediately when input no longer matches suggestion", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.style.display, "block");
|
|
|
|
addon.adjustToInput("dox");
|
|
|
|
assert.equal(ghost.style.display, "none");
|
|
assert.equal(ghost.textContent, "");
|
|
assert.equal(addon.isActive(), false);
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("applyKeystroke: printable char trims ghost tail when buffer is unreliable (issue #906)", () => {
|
|
// Repro for issue #906: after Tab passes to shell and the typed-buffer
|
|
// is flagged unreliable, the ghost addon's currentInput is the only
|
|
// source of truth for what the user has typed since the last show().
|
|
// Without applyKeystroke, line 798's reliability gate prevents
|
|
// adjustToInput from firing and the ghost retains its show-time tail
|
|
// — when the next keystroke advances the cursor, the stale tail
|
|
// overlaps the just-typed glyph (e.g., typing 't' after 'systemctl s'
|
|
// makes the screen read 'systemctl sttop firewalld').
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("systemctl stop firewalld", "systemctl s");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.textContent, "top firewalld");
|
|
|
|
addon.applyKeystroke("t");
|
|
|
|
// Ghost tail must shrink by exactly one char so when the shell
|
|
// echoes 't', the next visible glyph after the cursor is 'o', not
|
|
// 't' (which would render as 'sttop').
|
|
assert.equal(ghost.textContent, "op firewalld");
|
|
assert.equal(addon.isActive(), true);
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("applyKeystroke: backspace re-grows ghost tail by one char", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "doc");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.textContent, "ker");
|
|
|
|
addon.applyKeystroke("\x7f");
|
|
|
|
assert.equal(ghost.textContent, "cker");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("applyKeystroke: Ctrl+W word-erases trailing word from currentInput", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// Mid-suggestion: user has typed two words; Ctrl+W should drop the
|
|
// tail word and let the ghost regrow to cover what was erased.
|
|
addon.show("git commit -m wip", "git com");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
assert.equal(ghost.textContent, "mit -m wip");
|
|
|
|
addon.applyKeystroke("\x17");
|
|
|
|
// The same /\s*\S+\s*$/ regex used by handleInput consumes the
|
|
// leading whitespace too, so "git com" → "git"; the ghost regrows
|
|
// to cover the now-uncovered leading space + remainder.
|
|
assert.equal(ghost.textContent, " commit -m wip");
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("applyKeystroke: hides ghost when next char diverges from suggestion", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
|
|
// 'x' breaks the prefix invariant — ghost must hide immediately so
|
|
// a → -accept after this point can't pull a stale tail onto a line
|
|
// that no longer matches the suggestion.
|
|
addon.applyKeystroke("x");
|
|
|
|
assert.equal(ghost.style.display, "none");
|
|
assert.equal(addon.isActive(), false);
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("applyKeystroke: ignores non-typing data (escape sequences, control codes)", () => {
|
|
// Escape sequences and other control codes are routed through
|
|
// clearState() in handleInput, not propagated to the ghost — but we
|
|
// want applyKeystroke to be a safe no-op if accidentally called with
|
|
// them (defense in depth).
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("docker", "do");
|
|
const ghost = ghostElement();
|
|
assert.ok(ghost);
|
|
const tailBefore = ghost.textContent;
|
|
|
|
addon.applyKeystroke("\x1b[A"); // up-arrow escape sequence
|
|
addon.applyKeystroke("\x01"); // Ctrl+A
|
|
addon.applyKeystroke(""); // empty
|
|
|
|
assert.equal(ghost.textContent, tailBefore);
|
|
assert.equal(addon.isActive(), true);
|
|
} finally {
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost on render when the device echoed untracked input (#1013)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// We believe only "network in" is typed; suggestion is the full command.
|
|
addon.show("network interface show", "network in");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
// The real line shows MORE than we tracked: a bastion host echoed the
|
|
// next char ("t") that our client-side buffer never recorded.
|
|
const line = "ecOS# network int";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = line.length;
|
|
active.getLine = () => ({ translateToString: () => line });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost when TopsecOS-style backspace leaves stale text after the cursor (#1060)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
// The user deleted back from "system..." to "syst", so the tracked
|
|
// input is shorter and the ghost regrows to "em license show".
|
|
addon.show("system license show", "syst");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
// TopsecOS devices shown in #1060 leave the old suffix visible after
|
|
// the cursor while processing Backspace, so the real buffer looks like:
|
|
// TopsecOS# syst|em license show
|
|
const beforeCursor = "TopsecOS# syst";
|
|
const line = `${beforeCursor}em license show`;
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => line });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost when only the deleted prefix remains after the cursor (#1060)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "syst");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "TopsecOS# syst";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor}em` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost when TopsecOS-style backspace leaves stale text after empty input (#1060)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "TopsecOS# ";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor}system license show` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost when stale text is followed by right-side status text (#1060)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "syst");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "TopsecOS# syst";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor}em 12:34 ok` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("hides the ghost when a stale argument suffix starts with a space (#1060)", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "system");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "TopsecOS# system";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor} license show` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), false);
|
|
assert.equal(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("keeps the ghost when unrelated right-side prompt text is visible", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "syst");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "host# syst";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor} 12:34 ok` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), true);
|
|
assert.notEqual(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("keeps the ghost when only right-side spacing overlaps a space-prefixed suffix", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "system");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "host# system";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor} 12:34 ok` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), true);
|
|
assert.notEqual(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|
|
|
|
test("keeps the ghost when adjacent right-side text only shares the first character", () => {
|
|
const restoreDocument = installFakeDocument();
|
|
const { term, ghostElement, fireRender } = createFakeTerm();
|
|
const addon = new GhostTextAddon();
|
|
|
|
try {
|
|
addon.activate(term as never);
|
|
addon.show("system license show", "syst");
|
|
assert.equal(addon.isActive(), true);
|
|
|
|
const beforeCursor = "host# syst";
|
|
const active = term.buffer.active as Record<string, unknown>;
|
|
active.baseY = 0;
|
|
active.cursorX = beforeCursor.length;
|
|
active.getLine = () => ({ translateToString: () => `${beforeCursor}error` });
|
|
|
|
fireRender();
|
|
|
|
assert.equal(addon.isActive(), true);
|
|
assert.notEqual(ghostElement()?.style.display, "none");
|
|
} finally {
|
|
addon.dispose();
|
|
restoreDocument();
|
|
}
|
|
});
|