first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
import { afterEach, describe, expect, it, vi } from "vitest";
type TerminalPageOptions = {
data?: Record<string, unknown>;
[key: string]: unknown;
};
type TerminalCaretSnapshot = {
left: number;
top: number;
height: number;
visible: boolean;
cursorRow: number;
cursorCol: number;
scrollTop: number;
rawTop: number;
rawLeft: number;
rectWidth: number;
rectHeight: number;
lineHeight: number;
charWidth: number;
};
type TerminalPageInstance = TerminalPageOptions & {
data: Record<string, unknown>;
activeTerminalStdoutTask: Record<string, unknown> | null;
terminalStableCaretSnapshot: TerminalCaretSnapshot | null;
terminalPendingCaretSnapshot: TerminalCaretSnapshot | null;
terminalPendingCaretSince: number;
shellLineHeightPx?: number;
terminalPerf?: Record<string, unknown> | null;
setData: (patch: Record<string, unknown>, callback?: () => void) => void;
resolveStableTerminalCaret: (
caret: TerminalCaretSnapshot,
options?: Record<string, unknown>
) => TerminalCaretSnapshot | null;
resolveActivationBandFromCaretSnapshot: (
rect: Record<string, unknown>,
caret: TerminalCaretSnapshot | null,
cursorMetrics?: Record<string, unknown> | null
) => { top: number; height: number };
resetTerminalCaretStabilityState: () => void;
syncTerminalOverlay: (options?: Record<string, unknown>, callback?: (perf?: Record<string, unknown>) => void) => void;
resolveOutputScrollTopForRect: ReturnType<typeof vi.fn>;
getTerminalModes: ReturnType<typeof vi.fn>;
shouldLogTerminalPerfFrame: ReturnType<typeof vi.fn>;
logTerminalPerf: ReturnType<typeof vi.fn>;
};
type MiniprogramGlobals = typeof globalThis & {
Page?: (options: TerminalPageOptions) => void;
wx?: Record<string, unknown>;
};
function buildCaretSnapshot(top: number, left = 12): TerminalCaretSnapshot {
return {
left,
top,
height: 21,
visible: true,
cursorRow: Math.max(0, Math.round(top / 21)),
cursorCol: Math.max(0, Math.round(left / 9)),
scrollTop: 0,
rawTop: top,
rawLeft: left,
rectWidth: 320,
rectHeight: 480,
lineHeight: 21,
charWidth: 9
};
}
function createTerminalPageHarness() {
const globalState = globalThis as MiniprogramGlobals;
let capturedPageOptions: TerminalPageOptions | null = null;
const noop = () => {};
vi.resetModules();
delete require.cache[require.resolve("./index.js")];
globalState.Page = vi.fn((options: TerminalPageOptions) => {
capturedPageOptions = options;
});
globalState.wx = {
env: {
USER_DATA_PATH: "/tmp"
},
getStorageSync: vi.fn(() => undefined),
setStorageSync: vi.fn(),
removeStorageSync: vi.fn(),
getRecorderManager: vi.fn(() => ({
onStart: noop,
onStop: noop,
onError: noop,
onFrameRecorded: noop,
start: noop,
stop: noop
})),
createInnerAudioContext: vi.fn(() => ({
onCanplay: noop,
onPlay: noop,
onEnded: noop,
onStop: noop,
onError: noop,
stop: noop,
destroy: noop
})),
setInnerAudioOption: vi.fn(),
createSelectorQuery: vi.fn(() => ({
in: vi.fn(() => ({
select: vi.fn(() => ({
boundingClientRect: vi.fn(() => ({
exec: noop
}))
}))
}))
})),
nextTick: vi.fn((callback?: () => void) => {
callback?.();
}),
getSystemInfoSync: vi.fn(() => ({})),
canIUse: vi.fn(() => false)
};
require("./index.js");
if (!capturedPageOptions) {
throw new Error("terminal page not captured");
}
const captured = capturedPageOptions as TerminalPageOptions;
const page = {
...captured,
data: JSON.parse(JSON.stringify(captured.data || {})) as Record<string, unknown>,
activeTerminalStdoutTask: null,
terminalStableCaretSnapshot: null,
terminalPendingCaretSnapshot: null,
terminalPendingCaretSince: 0,
shellLineHeightPx: 21,
terminalPerf: null,
setData(patch: Record<string, unknown>, callback?: () => void) {
Object.assign(this.data, patch);
callback?.();
}
} as TerminalPageInstance;
page.resolveOutputScrollTopForRect = vi.fn(() => 0);
page.getTerminalModes = vi.fn(() => ({ cursorHidden: false }));
page.shouldLogTerminalPerfFrame = vi.fn(() => false);
page.logTerminalPerf = vi.fn();
return { page };
}
describe("terminal caret stability", () => {
const globalState = globalThis as MiniprogramGlobals;
const originalPage = globalState.Page;
const originalWx = globalState.wx;
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
if (originalPage) {
globalState.Page = originalPage;
} else {
delete globalState.Page;
}
if (originalWx) {
globalState.wx = originalWx;
} else {
delete globalState.wx;
}
});
it("stdout in-flight 时,短时间跳到新位置仍保留上一个稳定 caret", () => {
let now = 1000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const { page } = createTerminalPageHarness();
page.activeTerminalStdoutTask = {
remainingText: "working"
};
const first = page.resolveStableTerminalCaret(buildCaretSnapshot(210), {
stabilizeDuringStdout: true
});
now = 1040;
const second = page.resolveStableTerminalCaret(buildCaretSnapshot(252), {
stabilizeDuringStdout: true
});
expect(first?.top).toBe(210);
expect(second?.top).toBe(210);
});
it("同一位置超过稳定窗口后,会提交新的 caret 位置", () => {
let now = 2000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const { page } = createTerminalPageHarness();
page.activeTerminalStdoutTask = {
remainingText: "working"
};
page.resolveStableTerminalCaret(buildCaretSnapshot(210), {
stabilizeDuringStdout: true
});
now = 2040;
page.resolveStableTerminalCaret(buildCaretSnapshot(252), {
stabilizeDuringStdout: true
});
now = 2180;
const stabilized = page.resolveStableTerminalCaret(buildCaretSnapshot(252), {
stabilizeDuringStdout: true
});
expect(stabilized?.top).toBe(252);
});
it("最终帧会强制提交最新 caret不再继续冻结旧位置", () => {
let now = 3000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const { page } = createTerminalPageHarness();
page.activeTerminalStdoutTask = {
remainingText: "working"
};
page.resolveStableTerminalCaret(buildCaretSnapshot(210), {
stabilizeDuringStdout: true
});
now = 3040;
page.resolveStableTerminalCaret(buildCaretSnapshot(252), {
stabilizeDuringStdout: true
});
now = 3060;
const finalCaret = page.resolveStableTerminalCaret(buildCaretSnapshot(252), {
stabilizeDuringStdout: true,
forceCommit: true
});
expect(finalCaret?.top).toBe(252);
});
it("stdout in-flight 时,激活框应跟随稳定 caret而不是按实时位置单独跳动", () => {
let now = 4000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const { page } = createTerminalPageHarness();
page.activeTerminalStdoutTask = {
remainingText: "working"
};
page.data.statusClass = "connected";
page.data.activationDebugEnabled = true;
page.data.activationDebugVisible = true;
page.data.terminalCaretVisible = false;
page.data.terminalCaretTopPx = 0;
page.data.activationDebugTopPx = 0;
page.data.activationDebugHeightPx = 0;
const rect = {
width: 320,
height: 480
};
page.syncTerminalOverlay({
rect,
cursorMetrics: {
lineHeight: 21,
charWidth: 9,
paddingLeft: 12,
paddingRight: 8,
cursorRow: 10,
cursorCol: 1,
rows: 20
},
stabilizeCaretDuringStdout: true
});
expect(page.data.terminalCaretTopPx).toBe(210);
expect(page.data.activationDebugTopPx).toBe(168);
now = 4040;
page.syncTerminalOverlay({
rect,
cursorMetrics: {
lineHeight: 21,
charWidth: 9,
paddingLeft: 12,
paddingRight: 8,
cursorRow: 12,
cursorCol: 1,
rows: 20
},
stabilizeCaretDuringStdout: true
});
expect(page.data.terminalCaretTopPx).toBe(210);
expect(page.data.activationDebugTopPx).toBe(168);
});
});