152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { createTerminalRenderScheduler, mergeTerminalRenderOptions } = require("./terminalRenderScheduler.js");
|
|
|
|
describe("terminalRenderScheduler", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("会把渲染选项按单一真相源合并", () => {
|
|
expect(mergeTerminalRenderOptions(null, null)).toEqual({ sendResize: false });
|
|
expect(mergeTerminalRenderOptions({ sendResize: false }, { sendResize: true })).toEqual({
|
|
sendResize: true
|
|
});
|
|
expect(mergeTerminalRenderOptions({ sendResize: true }, { sendResize: false })).toEqual({
|
|
sendResize: true
|
|
});
|
|
});
|
|
|
|
it("stdout 高频输出会在一个批窗口内合并成一次渲染", () => {
|
|
const runs = [];
|
|
const scheduler = createTerminalRenderScheduler({
|
|
batchWindowMs: 16,
|
|
runRender(request, done) {
|
|
runs.push(request);
|
|
done({ ok: true });
|
|
}
|
|
});
|
|
|
|
scheduler.requestStdout({ appendStartedAt: 10, visibleBytes: 12 });
|
|
scheduler.requestStdout({ appendStartedAt: 12, visibleBytes: 18 });
|
|
|
|
expect(runs).toHaveLength(0);
|
|
|
|
vi.advanceTimersByTime(16);
|
|
|
|
expect(runs).toHaveLength(1);
|
|
expect(runs[0].reason).toBe("stdout_batch");
|
|
expect(runs[0].stdoutSamples).toHaveLength(2);
|
|
expect(runs[0].stdoutSamples[0]).toMatchObject({ appendStartedAt: 10, visibleBytes: 12 });
|
|
expect(runs[0].stdoutSamples[1]).toMatchObject({ appendStartedAt: 12, visibleBytes: 18 });
|
|
});
|
|
|
|
it("进行中的渲染完成后,只会补跑一轮合并后的后续请求", () => {
|
|
const runs = [];
|
|
const finishes = [];
|
|
const callbackMarks = [];
|
|
const scheduler = createTerminalRenderScheduler({
|
|
batchWindowMs: 16,
|
|
runRender(request, done) {
|
|
runs.push(request);
|
|
finishes.push(done);
|
|
}
|
|
});
|
|
|
|
scheduler.requestImmediate({}, (_result, request) => {
|
|
callbackMarks.push(`first:${request.reason}`);
|
|
});
|
|
expect(runs).toHaveLength(1);
|
|
|
|
scheduler.requestImmediate({ sendResize: true }, (_result, request) => {
|
|
callbackMarks.push(`second:${request.reason}:${request.stdoutSamples.length}`);
|
|
});
|
|
scheduler.requestStdout({ appendStartedAt: 20, visibleBytes: 5 });
|
|
|
|
expect(runs).toHaveLength(1);
|
|
|
|
finishes.shift()({ ok: true });
|
|
|
|
expect(runs).toHaveLength(2);
|
|
expect(runs[1].reason).toBe("pending_stdout");
|
|
expect(runs[1].options).toEqual({ sendResize: true });
|
|
expect(runs[1].stdoutSamples).toHaveLength(1);
|
|
|
|
finishes.shift()({ ok: true });
|
|
|
|
expect(callbackMarks).toEqual(["first:immediate", "second:pending_stdout:1"]);
|
|
});
|
|
|
|
it("普通立即渲染会抢占尚未触发的 stdout 定时批处理", () => {
|
|
const runs = [];
|
|
const scheduler = createTerminalRenderScheduler({
|
|
batchWindowMs: 16,
|
|
runRender(request, done) {
|
|
runs.push(request);
|
|
done({ ok: true });
|
|
}
|
|
});
|
|
|
|
scheduler.requestStdout({ appendStartedAt: 10, visibleBytes: 3 });
|
|
scheduler.requestImmediate({ sendResize: true });
|
|
|
|
expect(runs).toHaveLength(1);
|
|
expect(runs[0].reason).toBe("immediate");
|
|
expect(runs[0].options).toEqual({ sendResize: true });
|
|
expect(runs[0].stdoutSamples).toHaveLength(1);
|
|
|
|
vi.advanceTimersByTime(16);
|
|
|
|
expect(runs).toHaveLength(1);
|
|
});
|
|
|
|
it("支持输出当前 pending 与 in-flight 的调度快照,便于慢场景诊断", () => {
|
|
const finishes = [];
|
|
let now = 100;
|
|
const scheduler = createTerminalRenderScheduler({
|
|
batchWindowMs: 16,
|
|
now: () => now,
|
|
runRender(request, done) {
|
|
finishes.push(done);
|
|
}
|
|
});
|
|
|
|
scheduler.requestStdout({ text: "你好", appendStartedAt: 80, visibleBytes: 6 });
|
|
now = 140;
|
|
expect(scheduler.getSnapshot()).toMatchObject({
|
|
inFlight: false,
|
|
pending: {
|
|
waitMs: 40,
|
|
stdoutSampleCount: 1,
|
|
stdoutRawBytes: 6
|
|
},
|
|
active: null
|
|
});
|
|
|
|
vi.advanceTimersByTime(16);
|
|
now = 180;
|
|
expect(scheduler.getSnapshot()).toMatchObject({
|
|
inFlight: true,
|
|
pending: null,
|
|
active: {
|
|
reason: "stdout_batch",
|
|
ageMs: 40,
|
|
waitMs: 40,
|
|
stdoutSampleCount: 1,
|
|
stdoutRawBytes: 6
|
|
}
|
|
});
|
|
|
|
finishes.shift()({ ok: true });
|
|
expect(scheduler.getSnapshot()).toMatchObject({
|
|
inFlight: false,
|
|
pending: null,
|
|
active: null
|
|
});
|
|
});
|
|
});
|