125 lines
5.4 KiB
TypeScript
125 lines
5.4 KiB
TypeScript
import { beforeAll, describe, expect, it } from "bun:test";
|
|
import { HookSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/hook-selector";
|
|
import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
|
import { visibleWidth } from "@oh-my-pi/pi-tui";
|
|
|
|
beforeAll(async () => {
|
|
const theme = await getThemeByName("dark");
|
|
if (!theme) {
|
|
throw new Error("Failed to load dark theme for tests");
|
|
}
|
|
setThemeInstance(theme);
|
|
});
|
|
describe("HookSelectorComponent", () => {
|
|
it("keeps outlined options within render width", () => {
|
|
const options = [
|
|
"aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;b",
|
|
"bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;a",
|
|
"a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b",
|
|
];
|
|
const component = new HookSelectorComponent(
|
|
"Which pattern do you prefer?",
|
|
options,
|
|
() => {},
|
|
() => {},
|
|
{ outline: true, initialIndex: 0 },
|
|
);
|
|
|
|
const width = 80;
|
|
const lines = component.render(width);
|
|
for (const line of lines) {
|
|
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
|
|
}
|
|
});
|
|
|
|
it("wraps outlined option text without omitting the tail", () => {
|
|
const options = [
|
|
"Option A: Move to OMP-native only by migrating reusable shared AI instructions into .omp/AGENTS.md, .omp/rules, .omp/skills, and .omp/agents while deliberately not creating a root .github directory.",
|
|
"Option B: Keep dual support by migrating canonical instructions into .omp while also maintaining a root .github/copilot-instructions.md compatibility bridge for editors that do not understand OMP resources yet.",
|
|
];
|
|
const component = new HookSelectorComponent(
|
|
"Which migration stance should be used?",
|
|
options,
|
|
() => {},
|
|
() => {},
|
|
{ outline: true, initialIndex: 0 },
|
|
);
|
|
|
|
const width = 72;
|
|
const lines = component.render(width);
|
|
const plain = lines.map(line => Bun.stripANSI(line)).join("\n");
|
|
const normalizedPlain = plain.replace(/[\u2500-\u257f]/g, " ").replace(/\s+/g, " ");
|
|
expect(normalizedPlain).toContain("not creating a root .github directory");
|
|
expect(normalizedPlain).toContain("do not understand OMP resources yet");
|
|
for (const line of lines) {
|
|
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
|
|
}
|
|
});
|
|
|
|
it("renders option descriptions as separate wrapped rows", () => {
|
|
const options = [
|
|
{
|
|
label: "Use existing local credentials",
|
|
description:
|
|
"Authenticate via the provider keys and OAuth state already configured under ~/.omp without opening a new browser-based setup flow.",
|
|
},
|
|
{
|
|
label: "Set up Oh My Pi in terminal",
|
|
description:
|
|
"Launch the local terminal UI to add provider keys, select models, and keep the current editor session waiting for the configured credentials.",
|
|
},
|
|
];
|
|
const component = new HookSelectorComponent(
|
|
"How should authentication continue?",
|
|
options,
|
|
() => {},
|
|
() => {},
|
|
{ outline: true, initialIndex: 0 },
|
|
);
|
|
|
|
const width = 76;
|
|
const lines = component.render(width);
|
|
const plainLines = lines.map(line => Bun.stripANSI(line));
|
|
const normalizedPlain = plainLines
|
|
.join("\n")
|
|
.replace(/[\u2500-\u257f]/g, " ")
|
|
.replace(/\s+/g, " ");
|
|
const labelLineIndex = plainLines.findIndex(line => line.includes("Use existing local credentials"));
|
|
const descriptionLineIndex = plainLines.findIndex(line => line.includes("Authenticate via the provider keys"));
|
|
expect(labelLineIndex).toBeGreaterThanOrEqual(0);
|
|
expect(descriptionLineIndex).toBeGreaterThan(labelLineIndex);
|
|
expect(normalizedPlain).toContain("without opening a new browser-based setup flow");
|
|
expect(normalizedPlain).toContain("keep the current editor session waiting for the configured credentials");
|
|
for (const line of lines) {
|
|
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
|
|
}
|
|
});
|
|
|
|
it("filters options by description text", () => {
|
|
const component = new HookSelectorComponent(
|
|
"Which setup path should be used?",
|
|
[
|
|
{ label: "Path A", description: "Reuse the credentials already available in the environment." },
|
|
{ label: "Path B", description: "Launch a browser flow to authorize a new provider account." },
|
|
{ label: "Path C", description: "Open the local settings file and edit provider keys manually." },
|
|
{ label: "Path D", description: "Skip provider setup and continue with offline-only tools." },
|
|
],
|
|
() => {},
|
|
() => {},
|
|
{ outline: true, maxVisible: 3 },
|
|
);
|
|
|
|
for (const key of "browser") {
|
|
component.handleInput(key);
|
|
}
|
|
|
|
const plain = component
|
|
.render(76)
|
|
.map(line => Bun.stripANSI(line))
|
|
.join("\n");
|
|
expect(plain).toContain("Path B");
|
|
expect(plain).toContain("Launch a browser flow");
|
|
expect(plain).not.toContain("Path A");
|
|
});
|
|
});
|