Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
223 lines
6.6 KiB
TypeScript
223 lines
6.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
import {
|
|
TERMINAL_TIMESTAMP_GUTTER_HORIZONTAL_PADDING,
|
|
TERMINAL_TIMESTAMP_GUTTER_MIN_WIDTH,
|
|
getTerminalTimestampTypography,
|
|
resolveTerminalTimestampGutterRenderSignature,
|
|
resolveTerminalTimestampGutterColor,
|
|
resolveTerminalTimestampGutterWidth,
|
|
syncTerminalTimestampGutterRows,
|
|
} from "./TerminalTimestampGutter.tsx";
|
|
|
|
test("timestamp gutter uses a bright color from the active terminal theme", () => {
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterColor({
|
|
brightCyan: "#66e8ff",
|
|
brightYellow: "#ffe066",
|
|
foreground: "#dddddd",
|
|
}),
|
|
"#66e8ff",
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter falls back within the terminal theme palette", () => {
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterColor({
|
|
brightYellow: "#ffe066",
|
|
foreground: "#dddddd",
|
|
}),
|
|
"#ffe066",
|
|
);
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterColor({
|
|
foreground: "#dddddd",
|
|
}),
|
|
"#dddddd",
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter width follows measured timestamp text width", () => {
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterWidth({ measuredTextWidth: 84, fontSize: 14 }),
|
|
84 + TERMINAL_TIMESTAMP_GUTTER_HORIZONTAL_PADDING,
|
|
);
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterWidth({ measuredTextWidth: 1, fontSize: 14 }),
|
|
TERMINAL_TIMESTAMP_GUTTER_MIN_WIDTH,
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter typography follows terminal typography", () => {
|
|
assert.deepEqual(
|
|
getTerminalTimestampTypography({
|
|
fontFamily: '"JetBrains Mono", monospace',
|
|
fontSize: 15,
|
|
fontWeight: 500,
|
|
}),
|
|
{
|
|
fontFamily: '"JetBrains Mono", monospace',
|
|
fontSize: 15,
|
|
fontWeight: 500,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter uses the terminal background", () => {
|
|
const source = readFileSync(new URL("./TerminalTimestampGutter.tsx", import.meta.url), "utf8");
|
|
|
|
assert.match(source, /backgroundColor: "var\(--terminal-ui-bg\)"/);
|
|
assert.doesNotMatch(source, /bg-black\/10/);
|
|
assert.match(source, /boxShadow: "inset -0\.5px 0 0 color-mix\(in srgb, var\(--terminal-ui-fg\) 8%, transparent\)"/);
|
|
assert.doesNotMatch(source, /border-r/);
|
|
});
|
|
|
|
test("timestamp gutter render signature is stable and changes only for visible inputs", () => {
|
|
const base = resolveTerminalTimestampGutterRenderSignature({
|
|
screenTop: 8,
|
|
cellHeight: 17,
|
|
color: "#66e8ff",
|
|
fontFamily: "JetBrains Mono",
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
rows: [
|
|
{ row: 0, label: "10:00:00" },
|
|
{ row: 2, label: "10:00:02" },
|
|
],
|
|
});
|
|
|
|
assert.equal(
|
|
resolveTerminalTimestampGutterRenderSignature({
|
|
screenTop: 8,
|
|
cellHeight: 17,
|
|
color: "#66e8ff",
|
|
fontFamily: "JetBrains Mono",
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
rows: [
|
|
{ row: 0, label: "10:00:00" },
|
|
{ row: 2, label: "10:00:02" },
|
|
],
|
|
}),
|
|
base,
|
|
);
|
|
assert.notEqual(
|
|
resolveTerminalTimestampGutterRenderSignature({
|
|
screenTop: 8,
|
|
cellHeight: 17,
|
|
color: "#66e8ff",
|
|
fontFamily: "JetBrains Mono",
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
rows: [
|
|
{ row: 0, label: "10:00:00" },
|
|
{ row: 3, label: "10:00:02" },
|
|
],
|
|
}),
|
|
base,
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter flood throttle advances even when the paint signature is unchanged", () => {
|
|
const source = readFileSync(new URL("./TerminalTimestampGutter.tsx", import.meta.url), "utf8");
|
|
// lastFloodRenderAt must move on every render attempt, before the signature early-return.
|
|
const renderStart = source.indexOf("const render = () => {");
|
|
const signatureReturn = source.indexOf("if (signature === lastRenderSignature) return;", renderStart);
|
|
const floodAdvance = source.indexOf("lastFloodRenderAt = performance.now();", renderStart);
|
|
assert.notEqual(renderStart, -1);
|
|
assert.notEqual(signatureReturn, -1);
|
|
assert.notEqual(floodAdvance, -1);
|
|
assert.ok(
|
|
floodAdvance < signatureReturn,
|
|
"flood throttle clock must advance before the unchanged-signature early return",
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter throttles output-driven scroll events under flood pressure", () => {
|
|
const source = readFileSync(new URL("./TerminalTimestampGutter.tsx", import.meta.url), "utf8");
|
|
assert.match(source, /term\.onScroll\?\.\(\(\) => \{/);
|
|
assert.match(source, /getTerminalOutputPressure\(term\)/);
|
|
// largeOutput only (time-bounded); longLine must not sticky-throttle user scrolls.
|
|
assert.match(source, /scheduleRender\(pressure\.largeOutput \? "normal" : "immediate"\)/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/onScroll[\s\S]{0,200}pressure\.largeOutput \|\| pressure\.longLine/,
|
|
);
|
|
});
|
|
|
|
test("timestamp gutter reuses row nodes across paints instead of rebuilding the tree", () => {
|
|
const gutter = {
|
|
children: [] as Array<Record<string, unknown>>,
|
|
appendChild(node: Record<string, unknown>) {
|
|
this.children.push(node);
|
|
return node;
|
|
},
|
|
};
|
|
|
|
const createElement = (tag: string) => {
|
|
assert.equal(tag, "div");
|
|
return {
|
|
textContent: "",
|
|
className: "",
|
|
style: {} as Record<string, string>,
|
|
};
|
|
};
|
|
|
|
const previousCreateElement = globalThis.document?.createElement;
|
|
(globalThis as { document?: { createElement: typeof createElement } }).document = {
|
|
createElement,
|
|
};
|
|
|
|
try {
|
|
const layout = {
|
|
screenTop: 0,
|
|
cellHeight: 16,
|
|
color: "#66e8ff",
|
|
fontFamily: "monospace",
|
|
fontSize: 14,
|
|
fontWeight: 400,
|
|
};
|
|
|
|
syncTerminalTimestampGutterRows(
|
|
gutter as never,
|
|
[
|
|
{ row: 0, label: "10:00:00" },
|
|
{ row: 1, label: "10:00:01" },
|
|
],
|
|
layout,
|
|
);
|
|
assert.equal(gutter.children.length, 2);
|
|
const firstNode = gutter.children[0];
|
|
assert.equal(firstNode.textContent, "10:00:00");
|
|
|
|
syncTerminalTimestampGutterRows(
|
|
gutter as never,
|
|
[
|
|
{ row: 0, label: "10:00:02" },
|
|
{ row: 2, label: "10:00:03" },
|
|
],
|
|
layout,
|
|
);
|
|
assert.equal(gutter.children.length, 2);
|
|
assert.equal(gutter.children[0], firstNode);
|
|
assert.equal(firstNode.textContent, "10:00:02");
|
|
assert.equal(gutter.children[1].textContent, "10:00:03");
|
|
|
|
syncTerminalTimestampGutterRows(
|
|
gutter as never,
|
|
[{ row: 0, label: "10:00:04" }],
|
|
layout,
|
|
);
|
|
assert.equal(gutter.children.length, 2);
|
|
assert.equal((gutter.children[1].style as Record<string, string>).display, "none");
|
|
} finally {
|
|
if (previousCreateElement) {
|
|
(globalThis as { document: { createElement: typeof previousCreateElement } }).document = {
|
|
createElement: previousCreateElement,
|
|
};
|
|
}
|
|
}
|
|
});
|