Files
stanp-opencode/tests/tui.test.ts
T
StanPonomarev 59da616bc5
CI / verify (pull_request) Successful in 8s
reduce testing
2026-10-08 09:34:13 +02:00

162 lines
5.8 KiB
TypeScript

import { expect, test } from "bun:test";
import type { Plugin } from "@opencode/plugin/tui";
import type { Route, SlotMap } from "@opencode/plugin/tui/context";
import { testRender, type JSX } from "@opentui/solid";
import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
type SidebarSlotRender = (input: { readonly sessionID: string }) => JSX.Element;
type HomeSlotRender = (input: SlotMap["prompt.footer"]) => JSX.Element;
const status = (plan: string, remaining: number): QuotaStatus => ({
plan,
fetchedAt: 1,
windows: [
{
id: "codex:primary",
label: null,
seconds: 18_000,
used: 100 - remaining,
resetsAt: 2,
remaining,
},
],
});
test("built quota slot stays reactive across pending, empty, failure, update, and session states", async () => {
await build();
// Resolve the built bundle at runtime; tsc runs before build creates dist/tui.js.
const tui: typeof import("../src/tui.js") = await import(
new URL("../dist/tui.js", import.meta.url).href
);
const { default: plugin } = tui;
const pending = Promise.withResolvers<QuotaStatus | null>();
const responses: Array<() => Promise<QuotaStatus | null>> = [
async () => status("home", 90),
() => pending.promise,
];
let statusCalls = 0;
let sidebarRender: SidebarSlotRender | undefined;
let homeRender: HomeSlotRender | undefined;
let notifyUpdated: (() => void) | undefined;
const [route, setRoute] = createSignal<Route>({ type: "home" });
const quota = {
status() {
statusCalls++;
const response = responses.shift();
if (!response) throw new Error("Missing test response");
return response();
},
events: {
on(_event: "updated", callback: () => void) {
notifyUpdated = callback;
return () => undefined;
},
},
};
const contextFixture = {
client: { rpc: () => quota },
ui: {
slot(claim: {
after?: "prompt.footer";
append?: "sidebar.footer";
render: HomeSlotRender | SidebarSlotRender;
}) {
if (claim.after === "prompt.footer") {
// SAFETY: The prompt footer renderer receives the matching slot input.
homeRender = claim.render as HomeSlotRender;
} else {
// SAFETY: The only other fixture path is the session-aware sidebar slot.
sidebarRender = claim.render as SidebarSlotRender;
}
return () => undefined;
},
router: { current: route },
},
theme: { text: { default: "#ffffff", muted: "#888888" } },
};
// SAFETY: The slot setup only reads the client RPC, slot registration, and theme
// members implemented by this focused rendering fixture.
const context = Object.assign({} as Plugin.Context, contextFixture);
const cleanup = await plugin.setup(context);
if (!sidebarRender || !homeRender || !notifyUpdated)
throw new Error("Plugin did not register its slots");
const renderHome = homeRender;
const home = await testRender(() => renderHome({ mode: "normal", showDetails: false }), {
width: 40,
height: 6,
});
await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
const homeFrame = home.captureCharFrame();
expect(homeFrame.split("\n")).toEqual([
" ",
" ",
" ChatGPT quota (home) ",
" 5h: 90% · due ",
" ",
" ",
"",
]);
expect(statusCalls).toBe(1);
home.renderer.destroy();
setRoute({ type: "session", sessionID: "session-one" });
const sessionPrompt = await testRender(
() =>
renderHome({
sessionID: "session-one",
mode: "normal",
showDetails: false,
}),
{ width: 40, height: 5 },
);
await sessionPrompt.renderOnce();
expect(sessionPrompt.captureCharFrame()).not.toContain("ChatGPT quota");
expect(statusCalls).toBe(1);
sessionPrompt.renderer.destroy();
const [sessionID, setSessionID] = createSignal("session-one");
const renderSlot = sidebarRender;
const setup = await testRender(
() =>
renderSlot({
get sessionID() {
return sessionID();
},
}),
{ width: 70, height: 5 },
);
try {
await setup.renderOnce();
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
pending.resolve(status("plus", 75));
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
expect(setup.captureCharFrame()).toContain("5h: 75% · due");
expect(setup.captureCharFrame()).not.toContain("~");
responses.push(async () => null);
notifyUpdated();
await setup.waitForFrame((frame) => !frame.includes("ChatGPT quota"));
responses.push(async () => {
throw new Error("disconnected");
});
notifyUpdated();
await setup.waitFor(() => statusCalls === 4);
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
responses.push(async () => status("pro", 42));
setSessionID("session-two");
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
expect(statusCalls).toBe(5);
} finally {
setup.renderer.destroy();
if (cleanup) await cleanup();
}
});