Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
250 lines
7.6 KiB
TypeScript
250 lines
7.6 KiB
TypeScript
import assert from "node:assert";
|
|
import { describe, it } from "node:test";
|
|
import { Editor, type EditorTheme } from "../src/components/editor.ts";
|
|
import { Input } from "../src/components/input.ts";
|
|
import { SelectList, type SelectListTheme } from "../src/components/select-list.ts";
|
|
import { SettingsList, type SettingsListTheme } from "../src/components/settings-list.ts";
|
|
import { Container, type TuiMouseEvent, type TuiMouseEventType } from "../src/tui.ts";
|
|
import { TuiAltScreen } from "../src/tui-alt-screen.ts";
|
|
import { VirtualTerminal } from "./virtual-terminal.ts";
|
|
|
|
function mouse(type: TuiMouseEventType, x: number, y: number, width = 80, height = 10): TuiMouseEvent {
|
|
return {
|
|
type,
|
|
button: "left",
|
|
x,
|
|
y,
|
|
screenX: x,
|
|
screenY: y,
|
|
width,
|
|
height,
|
|
shift: false,
|
|
alt: false,
|
|
ctrl: false,
|
|
...(type === "click" ? { clickCount: 1 } : {}),
|
|
};
|
|
}
|
|
|
|
const selectTheme: SelectListTheme = {
|
|
selectedPrefix: (text) => text,
|
|
selectedText: (text) => text,
|
|
description: (text) => text,
|
|
scrollInfo: (text) => text,
|
|
noMatch: (text) => text,
|
|
};
|
|
|
|
const settingsTheme: SettingsListTheme = {
|
|
label: (text) => text,
|
|
value: (text) => text,
|
|
description: (text) => text,
|
|
cursor: "> ",
|
|
hint: (text) => text,
|
|
};
|
|
|
|
const editorTheme: EditorTheme = {
|
|
borderColor: (text) => text,
|
|
selectList: selectTheme,
|
|
};
|
|
|
|
class InputOverlay extends Container {
|
|
readonly input = new Input();
|
|
|
|
constructor() {
|
|
super();
|
|
this.addChild(this.input);
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
this.input.handleInput(data);
|
|
}
|
|
}
|
|
|
|
describe("mouse-aware components", () => {
|
|
it("positions a single-line input cursor on press", () => {
|
|
const input = new Input();
|
|
input.setValue("hello");
|
|
input.render(20);
|
|
|
|
assert.strictEqual(input.handleMouse(mouse("press", 4, 0, 20, 1))?.handled, true);
|
|
input.handleInput("X");
|
|
assert.strictEqual(input.getValue(), "heXllo");
|
|
});
|
|
|
|
it("selects and activates list rows", () => {
|
|
const list = new SelectList(
|
|
[
|
|
{ value: "a", label: "A" },
|
|
{ value: "b", label: "B" },
|
|
{ value: "c", label: "C" },
|
|
{ value: "d", label: "D" },
|
|
{ value: "e", label: "E" },
|
|
],
|
|
3,
|
|
selectTheme,
|
|
);
|
|
let selected: string | undefined;
|
|
list.onSelect = (item) => {
|
|
selected = item.value;
|
|
};
|
|
|
|
assert.strictEqual(list.handleMouse(mouse("press", 1, 2, 40, 3))?.handled, true);
|
|
assert.strictEqual(list.getSelectedItem()?.value, "c");
|
|
assert.strictEqual(list.handleMouse(mouse("click", 1, 2, 40, 3))?.handled, true);
|
|
assert.strictEqual(selected, "c");
|
|
});
|
|
|
|
it("activates settings rows", () => {
|
|
const changes: Array<{ id: string; value: string }> = [];
|
|
const list = new SettingsList(
|
|
[
|
|
{ id: "mode", label: "Mode", currentValue: "one", values: ["one", "two"] },
|
|
{ id: "other", label: "Other", currentValue: "off", values: ["off", "on"] },
|
|
{ id: "third", label: "Third", currentValue: "low", values: ["low", "high"] },
|
|
{ id: "fourth", label: "Fourth", currentValue: "x", values: ["x", "y"] },
|
|
],
|
|
3,
|
|
settingsTheme,
|
|
(id, value) => changes.push({ id, value }),
|
|
() => {},
|
|
);
|
|
|
|
list.handleMouse(mouse("press", 1, 2, 40, 5));
|
|
list.handleMouse(mouse("click", 1, 2, 40, 5));
|
|
assert.deepStrictEqual(changes, [{ id: "third", value: "high" }]);
|
|
});
|
|
|
|
for (const row of [0, 4]) {
|
|
it(`ignores hover and clicks visible select-list row ${row} after scrolling`, () => {
|
|
const list = new SelectList(
|
|
Array.from({ length: 12 }, (_, i) => ({ value: `item-${i}`, label: `Item ${i}` })),
|
|
5,
|
|
selectTheme,
|
|
);
|
|
const changes: string[] = [];
|
|
let selected: string | undefined;
|
|
list.onSelectionChange = (item) => changes.push(item.value);
|
|
list.onSelect = (item) => {
|
|
selected = item.value;
|
|
};
|
|
list.setSelectedIndex(5);
|
|
list.handleMouse({ ...mouse("wheel", 1, row), wheelDelta: 1 });
|
|
assert.strictEqual(list.getSelectedItem()?.value, "item-6");
|
|
assert.deepStrictEqual(changes, ["item-6"]);
|
|
const before = list.render(80);
|
|
assert.match(before[row], new RegExp(`Item ${4 + row}$`));
|
|
|
|
for (const y of [0, 1, 2, 3, 4, row]) {
|
|
assert.strictEqual(list.handleMouse({ ...mouse("move", 1, y), button: "none" }), undefined);
|
|
assert.deepStrictEqual(list.render(80), before);
|
|
}
|
|
assert.strictEqual(list.getSelectedItem()?.value, "item-6");
|
|
assert.deepStrictEqual(changes, ["item-6"]);
|
|
assert.strictEqual(selected, undefined);
|
|
|
|
list.handleMouse(mouse("press", 1, row));
|
|
list.render(80);
|
|
list.handleMouse(mouse("click", 1, row));
|
|
assert.strictEqual(selected, `item-${4 + row}`);
|
|
assert.deepStrictEqual(changes, ["item-6", `item-${4 + row}`]);
|
|
});
|
|
|
|
it(`ignores hover and clicks visible settings row ${row} after scrolling`, () => {
|
|
const changes: Array<{ id: string; value: string }> = [];
|
|
const list = new SettingsList(
|
|
Array.from({ length: 12 }, (_, i) => ({
|
|
id: `item-${i}`,
|
|
label: `Item ${i}`,
|
|
description: `Description ${i}`,
|
|
currentValue: "off",
|
|
values: ["off", "on"],
|
|
})),
|
|
5,
|
|
settingsTheme,
|
|
(id, value) => changes.push({ id, value }),
|
|
() => {},
|
|
{ enableSearch: true },
|
|
);
|
|
list.selectItem("item-5");
|
|
list.handleMouse({ ...mouse("wheel", 1, row + 2), wheelDelta: 1 });
|
|
const before = list.render(80);
|
|
assert.match(before[4], /^> Item 6/);
|
|
assert.match(before[row + 2], new RegExp(`Item ${4 + row} `));
|
|
|
|
for (const y of [0, 1, 2, 3, 4, row]) {
|
|
assert.strictEqual(list.handleMouse({ ...mouse("move", 1, y + 2), button: "none" }), undefined);
|
|
assert.deepStrictEqual(list.render(80), before);
|
|
}
|
|
assert.deepStrictEqual(changes, []);
|
|
|
|
list.handleMouse(mouse("press", 1, row + 2));
|
|
list.render(80);
|
|
list.handleMouse(mouse("click", 1, row + 2));
|
|
assert.deepStrictEqual(changes, [{ id: `item-${4 + row}`, value: "on" }]);
|
|
});
|
|
}
|
|
|
|
it("keeps a delegating overlay focused when its nested input is clicked", async () => {
|
|
const terminal = new VirtualTerminal(20, 4);
|
|
const tui = new TuiAltScreen(terminal);
|
|
const overlay = new InputOverlay();
|
|
overlay.input.setValue("hi");
|
|
tui.start();
|
|
tui.showOverlay(overlay, { anchor: "top-left", width: 20 });
|
|
await terminal.waitForRender();
|
|
|
|
terminal.sendInput("\x1b[<0;5;1M");
|
|
terminal.sendInput("\x1b[<0;5;1m");
|
|
terminal.sendInput("!");
|
|
await terminal.waitForRender();
|
|
|
|
assert.strictEqual(overlay.input.getValue(), "hi!");
|
|
assert.strictEqual(tui.getFocusedComponent(), overlay);
|
|
tui.stop();
|
|
});
|
|
|
|
it("positions and focuses the multiline editor through alternate-screen dispatch", async () => {
|
|
const terminal = new VirtualTerminal(20, 6);
|
|
const tui = new TuiAltScreen(terminal);
|
|
const editor = new Editor(tui, editorTheme);
|
|
editor.setText("hello");
|
|
tui.addChild(editor);
|
|
tui.start();
|
|
await terminal.waitForRender();
|
|
|
|
terminal.sendInput("\x1b[<0;3;2M");
|
|
terminal.sendInput("\x1b[<0;3;2m");
|
|
terminal.sendInput("X");
|
|
await terminal.waitForRender();
|
|
|
|
assert.strictEqual(editor.getText(), "heXllo");
|
|
assert.strictEqual(tui.getFocusedComponent(), editor);
|
|
tui.stop();
|
|
});
|
|
|
|
it("selects and copies editor text on drag instead of moving the cursor", async () => {
|
|
const terminal = new VirtualTerminal(20, 6);
|
|
const copied: string[] = [];
|
|
const tui = new TuiAltScreen(terminal, undefined, undefined, {
|
|
copySelection: async (text) => {
|
|
copied.push(text);
|
|
return true;
|
|
},
|
|
});
|
|
const editor = new Editor(tui, editorTheme);
|
|
editor.setText("hello world");
|
|
tui.addChild(editor);
|
|
tui.start();
|
|
await terminal.waitForRender();
|
|
const cursorBefore = editor.getCursor();
|
|
|
|
terminal.sendInput("\x1b[<0;1;2M");
|
|
terminal.sendInput("\x1b[<32;5;2M");
|
|
terminal.sendInput("\x1b[<0;5;2m");
|
|
await terminal.waitForRender();
|
|
|
|
assert.deepStrictEqual(copied, ["hello"]);
|
|
assert.deepStrictEqual(editor.getCursor(), cursorBefore);
|
|
tui.stop();
|
|
});
|
|
});
|