first commit
This commit is contained in:
436
apps/miniprogram/pages/terminal/vtInputHandler.test.ts
Normal file
436
apps/miniprogram/pages/terminal/vtInputHandler.test.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const { DEFAULT_TERMINAL_MODES } = require("./terminalBufferSet.js");
|
||||
const { createTerminalVtInputHandler } = require("./vtInputHandler.js");
|
||||
|
||||
describe("vtInputHandler", () => {
|
||||
it("OSC 10/11/12 查询会把响应写回队列", () => {
|
||||
const { handler, responses } = createHandler();
|
||||
|
||||
handler.handleOscSequence(10, "?;?;?");
|
||||
|
||||
expect(responses).toEqual([
|
||||
"\u001b]10;rgb:#112233\u001b\\",
|
||||
"\u001b]11;rgb:#445566\u001b\\",
|
||||
"\u001b]12;rgb:#778899\u001b\\"
|
||||
]);
|
||||
});
|
||||
|
||||
it("模式报告只回报当前真实维护的模式位", () => {
|
||||
const { handler, responses, runtimeState, setActiveBufferAlt } = createHandler();
|
||||
|
||||
runtimeState.modes.applicationCursorKeys = true;
|
||||
runtimeState.modes.sendFocus = true;
|
||||
runtimeState.modes.insertMode = true;
|
||||
setActiveBufferAlt(true);
|
||||
|
||||
handler.pushModeReport("?", 1);
|
||||
handler.pushModeReport("?", 1049);
|
||||
handler.pushModeReport("?", 1004);
|
||||
handler.pushModeReport("", 4);
|
||||
handler.pushModeReport("?", 9999);
|
||||
|
||||
expect(responses).toEqual([
|
||||
"\u001b[?1;1$y",
|
||||
"\u001b[?1049;1$y",
|
||||
"\u001b[?1004;1$y",
|
||||
"\u001b[4;1$y",
|
||||
"\u001b[?9999;0$y"
|
||||
]);
|
||||
});
|
||||
|
||||
it("1049 私有模式切换会先保存光标,再切屏并在退出时恢复", () => {
|
||||
const { handler, switchCalls, saveCalls, restoreCalls } = createHandler();
|
||||
|
||||
handler.handlePrivateMode(true, 1049);
|
||||
handler.handlePrivateMode(false, 1049);
|
||||
|
||||
expect(saveCalls.count).toBe(1);
|
||||
expect(restoreCalls.count).toBe(1);
|
||||
expect(switchCalls).toEqual([
|
||||
{ target: "alt", clearTarget: true },
|
||||
{ target: "normal", clearTarget: false }
|
||||
]);
|
||||
});
|
||||
|
||||
it("origin mode 私有模式切换会更新模式位,并委托运行态重置光标", () => {
|
||||
const { handler, runtimeState, originModeResets } = createHandler();
|
||||
|
||||
handler.handlePrivateMode(true, 6);
|
||||
handler.handlePrivateMode(false, 6);
|
||||
|
||||
expect(runtimeState.modes.originMode).toBe(false);
|
||||
expect(originModeResets).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it("DECSTR 软重置会收回模式位与保存光标,但不会改动当前 cursor", () => {
|
||||
const { handler, runtimeState, state, activeBuffer, defaults } = createHandler();
|
||||
|
||||
state.cursorRow = 7;
|
||||
state.cursorCol = 9;
|
||||
state.scrollTop = 3;
|
||||
state.scrollBottom = 8;
|
||||
state.ansiState = { fg: "#abcdef", bg: "#123456", bold: true, underline: true };
|
||||
activeBuffer.savedCursorRow = 5;
|
||||
activeBuffer.savedCursorCol = 6;
|
||||
activeBuffer.savedAnsiState = { fg: "#ffffff", bg: "#000000", bold: true, underline: false };
|
||||
runtimeState.modes = {
|
||||
...DEFAULT_TERMINAL_MODES,
|
||||
applicationCursorKeys: true,
|
||||
sendFocus: true,
|
||||
insertMode: true,
|
||||
cursorHidden: true
|
||||
};
|
||||
|
||||
handler.softResetTerminal();
|
||||
|
||||
expect(runtimeState.modes).toEqual(defaults);
|
||||
expect(state.cursorRow).toBe(7);
|
||||
expect(state.cursorCol).toBe(9);
|
||||
expect(state.scrollTop).toBe(0);
|
||||
expect(state.scrollBottom).toBe(23);
|
||||
expect(state.ansiState).toEqual({ fg: "", bg: "", bold: false, underline: false });
|
||||
expect(activeBuffer.savedCursorRow).toBe(0);
|
||||
expect(activeBuffer.savedCursorCol).toBe(0);
|
||||
expect(activeBuffer.savedAnsiState).toEqual({ fg: "", bg: "", bold: false, underline: false });
|
||||
});
|
||||
|
||||
it("光标移动类 CSI 会按默认参数分派到对应动作", () => {
|
||||
const { handler, cursorOps } = createHandler();
|
||||
|
||||
expect(handler.handleCursorControl("A", [])).toBe(true);
|
||||
expect(handler.handleCursorControl("B", [2])).toBe(true);
|
||||
expect(handler.handleCursorControl("C", [0])).toBe(true);
|
||||
expect(handler.handleCursorControl("D", [-3])).toBe(true);
|
||||
expect(handler.handleCursorControl("E", [])).toBe(true);
|
||||
expect(handler.handleCursorControl("F", [4])).toBe(true);
|
||||
|
||||
expect(cursorOps).toEqual([
|
||||
{ type: "up", value: 1 },
|
||||
{ type: "down", value: 2 },
|
||||
{ type: "right", value: 1 },
|
||||
{ type: "left", value: 1 },
|
||||
{ type: "nextLine", value: 1 },
|
||||
{ type: "previousLine", value: 4 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("HPA/VPA/CUP 会保留 VT 的 1-based 语义交给运行态落点", () => {
|
||||
const { handler, cursorOps } = createHandler();
|
||||
|
||||
expect(handler.handleCursorControl("G", [5])).toBe(true);
|
||||
expect(handler.handleCursorControl("d", [])).toBe(true);
|
||||
expect(handler.handleCursorControl("H", [3, 7])).toBe(true);
|
||||
expect(handler.handleCursorControl("f", [0, -2])).toBe(true);
|
||||
expect(handler.handleCursorControl("J", [2])).toBe(false);
|
||||
|
||||
expect(cursorOps).toEqual([
|
||||
{ type: "column1", value: 5 },
|
||||
{ type: "row1", value: 1 },
|
||||
{ type: "position1", row: 3, column: 7 },
|
||||
{ type: "position1", row: 1, column: 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("擦除类 CSI 会按各自默认参数分派到对应擦除动作", () => {
|
||||
const { handler, eraseOps } = createHandler();
|
||||
|
||||
expect(handler.handleEraseControl("J", [])).toBe(true);
|
||||
expect(handler.handleEraseControl("K", [2])).toBe(true);
|
||||
expect(handler.handleEraseControl("X", [0])).toBe(true);
|
||||
expect(handler.handleEraseControl("X", [-3])).toBe(true);
|
||||
expect(handler.handleEraseControl("P", [1])).toBe(false);
|
||||
|
||||
expect(eraseOps).toEqual([
|
||||
{ type: "display", value: 0 },
|
||||
{ type: "line", value: 2 },
|
||||
{ type: "chars", value: 1 },
|
||||
{ type: "chars", value: 1 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("编辑与滚动类 CSI 会按默认参数分派到对应 buffer 动作", () => {
|
||||
const { handler, editOps } = createHandler();
|
||||
|
||||
expect(handler.handleEditControl("@", [])).toBe(true);
|
||||
expect(handler.handleEditControl("P", [2])).toBe(true);
|
||||
expect(handler.handleEditControl("L", [0])).toBe(true);
|
||||
expect(handler.handleEditControl("M", [-3])).toBe(true);
|
||||
expect(handler.handleEditControl("S", [])).toBe(true);
|
||||
expect(handler.handleEditControl("T", [4])).toBe(true);
|
||||
expect(handler.handleEditControl("r", [2])).toBe(true);
|
||||
expect(handler.handleEditControl("r", [3, 0])).toBe(true);
|
||||
expect(handler.handleEditControl("u", [])).toBe(false);
|
||||
|
||||
expect(editOps).toEqual([
|
||||
{ type: "insertChars", value: 1 },
|
||||
{ type: "deleteChars", value: 2 },
|
||||
{ type: "insertLines", value: 1 },
|
||||
{ type: "deleteLines", value: 1 },
|
||||
{ type: "scrollUp", value: 1 },
|
||||
{ type: "scrollDown", value: 4 },
|
||||
{ type: "scrollRegion", top: 2, bottom: 24 },
|
||||
{ type: "scrollRegion", top: 3, bottom: 24 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("CSI s/u 与 ESC 7/8 会统一分派保存与恢复光标动作", () => {
|
||||
const { handler, saveCalls, restoreCalls } = createHandler();
|
||||
|
||||
expect(handler.handleCursorSaveRestoreControl("s")).toBe(true);
|
||||
expect(handler.handleCursorSaveRestoreControl("u")).toBe(true);
|
||||
expect(handler.handleCursorSaveRestoreControl("7")).toBe(true);
|
||||
expect(handler.handleCursorSaveRestoreControl("8")).toBe(true);
|
||||
expect(handler.handleCursorSaveRestoreControl("D")).toBe(false);
|
||||
|
||||
expect(saveCalls.count).toBe(2);
|
||||
expect(restoreCalls.count).toBe(2);
|
||||
});
|
||||
|
||||
it("查询类 CSI 会统一分派 mode report、DSR 和 DA", () => {
|
||||
const { handler, responses, runtimeState, setActiveBufferAlt } = createHandler();
|
||||
|
||||
runtimeState.modes.sendFocus = true;
|
||||
setActiveBufferAlt(true);
|
||||
|
||||
expect(handler.handleQueryControl("?", "$", "p", [1049])).toBe(true);
|
||||
expect(handler.handleQueryControl("?", "", "n", [6])).toBe(true);
|
||||
expect(handler.handleQueryControl("", "", "c", [0])).toBe(true);
|
||||
expect(handler.handleQueryControl(">", "", "c", [0])).toBe(true);
|
||||
expect(handler.handleQueryControl("!", "", "p", [0])).toBe(false);
|
||||
|
||||
expect(responses).toEqual([
|
||||
"\u001b[?1049;1$y",
|
||||
"\u001b[?2;3R",
|
||||
"\u001b[?1;2c",
|
||||
"\u001b[>0;276;0c"
|
||||
]);
|
||||
});
|
||||
|
||||
it("ESC D/E/M 与 ESC 7/8 会统一走 ESC 分派入口", () => {
|
||||
const { handler, saveCalls, restoreCalls, escOps } = createHandler();
|
||||
|
||||
expect(handler.handleEscControl("7")).toBe(true);
|
||||
expect(handler.handleEscControl("8")).toBe(true);
|
||||
expect(handler.handleEscControl("D")).toBe(true);
|
||||
expect(handler.handleEscControl("E")).toBe(true);
|
||||
expect(handler.handleEscControl("M")).toBe(true);
|
||||
expect(handler.handleEscControl("]")).toBe(false);
|
||||
|
||||
expect(saveCalls.count).toBe(1);
|
||||
expect(restoreCalls.count).toBe(1);
|
||||
expect(escOps).toEqual(["D", "E", "M"]);
|
||||
});
|
||||
|
||||
it("CSI 顶层分派会统一路由查询、模式切换和普通控制动作", () => {
|
||||
const { handler, responses, runtimeState, cursorOps, eraseOps, editOps, saveCalls, defaults } = createHandler();
|
||||
|
||||
runtimeState.modes.applicationCursorKeys = true;
|
||||
runtimeState.modes.insertMode = true;
|
||||
|
||||
expect(handler.handleCsiControl("!", "", "p", [0])).toBe(true);
|
||||
expect(runtimeState.modes).toEqual(defaults);
|
||||
|
||||
expect(handler.handleCsiControl("?", "", "h", [1])).toBe(true);
|
||||
expect(runtimeState.modes.applicationCursorKeys).toBe(true);
|
||||
|
||||
expect(handler.handleCsiControl("", "", "h", [4])).toBe(true);
|
||||
expect(runtimeState.modes.insertMode).toBe(true);
|
||||
|
||||
expect(handler.handleCsiControl("?", "$", "p", [1])).toBe(true);
|
||||
expect(handler.handleCsiControl("", "", "B", [2])).toBe(true);
|
||||
expect(handler.handleCsiControl("", "", "K", [])).toBe(true);
|
||||
expect(handler.handleCsiControl("", "", "@", [3])).toBe(true);
|
||||
expect(handler.handleCsiControl("", "", "s", [])).toBe(true);
|
||||
expect(handler.handleCsiControl(">", "", "u", [1])).toBe(false);
|
||||
|
||||
expect(responses).toEqual(["\u001b[?1;1$y"]);
|
||||
expect(cursorOps).toEqual([{ type: "down", value: 2 }]);
|
||||
expect(eraseOps).toEqual([{ type: "line", value: 0 }]);
|
||||
expect(editOps).toEqual([{ type: "insertChars", value: 3 }]);
|
||||
expect(saveCalls.count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
function createHandler() {
|
||||
const defaults = { ...DEFAULT_TERMINAL_MODES };
|
||||
const responses: string[] = [];
|
||||
const runtimeState = { modes: { ...defaults } };
|
||||
const normalBuffer = {
|
||||
isAlt: false,
|
||||
savedCursorRow: 0,
|
||||
savedCursorCol: 0,
|
||||
savedAnsiState: { fg: "", bg: "", bold: false, underline: false }
|
||||
};
|
||||
const altBuffer = {
|
||||
isAlt: true,
|
||||
savedCursorRow: 0,
|
||||
savedCursorCol: 0,
|
||||
savedAnsiState: { fg: "", bg: "", bold: false, underline: false }
|
||||
};
|
||||
const state = {
|
||||
cursorRow: 1,
|
||||
cursorCol: 2,
|
||||
scrollTop: 0,
|
||||
scrollBottom: 23,
|
||||
ansiState: { fg: "", bg: "", bold: false, underline: false }
|
||||
};
|
||||
let activeBuffer = normalBuffer;
|
||||
const switchCalls: Array<{ target: string; clearTarget: boolean }> = [];
|
||||
const saveCalls = { count: 0 };
|
||||
const restoreCalls = { count: 0 };
|
||||
const cursorOps: Array<
|
||||
| { type: "up" | "down" | "right" | "left" | "nextLine" | "previousLine"; value: number }
|
||||
| { type: "column1" | "row1"; value: number }
|
||||
| { type: "position1"; row: number; column: number }
|
||||
> = [];
|
||||
const eraseOps: Array<
|
||||
| { type: "display" | "line"; value: number }
|
||||
| { type: "chars"; value: number }
|
||||
> = [];
|
||||
const editOps: Array<
|
||||
| { type: "insertChars" | "deleteChars" | "insertLines" | "deleteLines" | "scrollUp" | "scrollDown"; value: number }
|
||||
| { type: "scrollRegion"; top: number; bottom: number }
|
||||
> = [];
|
||||
const escOps: string[] = [];
|
||||
const originModeResets: boolean[] = [];
|
||||
|
||||
const handler = createTerminalVtInputHandler({
|
||||
ansiResetState: { fg: "", bg: "", bold: false, underline: false },
|
||||
bufferCols: 80,
|
||||
bufferRows: 24,
|
||||
cloneAnsiState: (value: { fg?: string; bg?: string; bold?: boolean; underline?: boolean } | null) => ({
|
||||
fg: value && value.fg ? String(value.fg) : "",
|
||||
bg: value && value.bg ? String(value.bg) : "",
|
||||
bold: !!(value && value.bold),
|
||||
underline: !!(value && value.underline)
|
||||
}),
|
||||
getActiveBuffer: () => activeBuffer,
|
||||
getCursorCol: () => state.cursorCol,
|
||||
getScreenCursorRow: () => state.cursorRow,
|
||||
getScrollBottom: () => state.scrollBottom,
|
||||
getScrollTop: () => state.scrollTop,
|
||||
responses,
|
||||
restoreCurrentCursor: () => {
|
||||
restoreCalls.count += 1;
|
||||
},
|
||||
runtimeColors: {
|
||||
defaultForeground: "#112233",
|
||||
defaultBackground: "#445566",
|
||||
defaultCursor: "#778899"
|
||||
},
|
||||
runtimeState,
|
||||
saveCurrentCursor: () => {
|
||||
saveCalls.count += 1;
|
||||
},
|
||||
moveCursorUp: (value: number) => {
|
||||
cursorOps.push({ type: "up", value });
|
||||
},
|
||||
moveCursorDown: (value: number) => {
|
||||
cursorOps.push({ type: "down", value });
|
||||
},
|
||||
moveCursorRight: (value: number) => {
|
||||
cursorOps.push({ type: "right", value });
|
||||
},
|
||||
moveCursorLeft: (value: number) => {
|
||||
cursorOps.push({ type: "left", value });
|
||||
},
|
||||
moveCursorNextLine: (value: number) => {
|
||||
cursorOps.push({ type: "nextLine", value });
|
||||
},
|
||||
moveCursorPreviousLine: (value: number) => {
|
||||
cursorOps.push({ type: "previousLine", value });
|
||||
},
|
||||
setCursorColumn1: (value: number) => {
|
||||
cursorOps.push({ type: "column1", value });
|
||||
},
|
||||
setCursorRow1: (value: number) => {
|
||||
cursorOps.push({ type: "row1", value });
|
||||
},
|
||||
setCursorPosition1: (row: number, column: number) => {
|
||||
cursorOps.push({ type: "position1", row, column });
|
||||
},
|
||||
clearDisplayByMode: (value: number) => {
|
||||
eraseOps.push({ type: "display", value });
|
||||
},
|
||||
clearLineByMode: (value: number) => {
|
||||
eraseOps.push({ type: "line", value });
|
||||
},
|
||||
eraseChars: (value: number) => {
|
||||
eraseOps.push({ type: "chars", value });
|
||||
},
|
||||
insertChars: (value: number) => {
|
||||
editOps.push({ type: "insertChars", value });
|
||||
},
|
||||
deleteChars: (value: number) => {
|
||||
editOps.push({ type: "deleteChars", value });
|
||||
},
|
||||
insertLines: (value: number) => {
|
||||
editOps.push({ type: "insertLines", value });
|
||||
},
|
||||
deleteLines: (value: number) => {
|
||||
editOps.push({ type: "deleteLines", value });
|
||||
},
|
||||
scrollRegionUp: (value: number) => {
|
||||
editOps.push({ type: "scrollUp", value });
|
||||
},
|
||||
scrollRegionDown: (value: number) => {
|
||||
editOps.push({ type: "scrollDown", value });
|
||||
},
|
||||
setScrollRegion: (top: number, bottom: number) => {
|
||||
editOps.push({ type: "scrollRegion", top, bottom });
|
||||
},
|
||||
resetCursorForOriginMode: (enabled: boolean) => {
|
||||
originModeResets.push(enabled);
|
||||
},
|
||||
indexDown: () => {
|
||||
escOps.push("D");
|
||||
},
|
||||
nextLine: () => {
|
||||
escOps.push("E");
|
||||
},
|
||||
reverseIndex: () => {
|
||||
escOps.push("M");
|
||||
},
|
||||
setAnsiState: (value: { fg: string; bg: string; bold: boolean; underline: boolean }) => {
|
||||
state.ansiState = { ...value };
|
||||
},
|
||||
setCursorCol: (value: number) => {
|
||||
state.cursorCol = value;
|
||||
},
|
||||
setCursorRow: (value: number) => {
|
||||
state.cursorRow = value;
|
||||
},
|
||||
setScrollBottom: (value: number) => {
|
||||
state.scrollBottom = value;
|
||||
},
|
||||
setScrollTop: (value: number) => {
|
||||
state.scrollTop = value;
|
||||
},
|
||||
switchActiveBuffer: (target: string, clearTarget: boolean) => {
|
||||
switchCalls.push({ target, clearTarget });
|
||||
activeBuffer = target === "alt" ? altBuffer : normalBuffer;
|
||||
},
|
||||
toOscRgbString: (value: string) => `rgb:${value}`
|
||||
});
|
||||
|
||||
return {
|
||||
activeBuffer,
|
||||
defaults,
|
||||
handler,
|
||||
responses,
|
||||
restoreCalls,
|
||||
runtimeState,
|
||||
saveCalls,
|
||||
originModeResets,
|
||||
cursorOps,
|
||||
eraseOps,
|
||||
editOps,
|
||||
escOps,
|
||||
setActiveBufferAlt(value: boolean) {
|
||||
activeBuffer = value ? altBuffer : normalBuffer;
|
||||
},
|
||||
state,
|
||||
switchCalls
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user