Files
pi-ask/tests/render-question.test.ts
T

359 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { createInitialState } from "../src/state/create.ts";
import { getRenderableOptions } from "../src/state/selectors.ts";
import {
applyNumberShortcut,
enterOptionNoteMode,
enterQuestionNoteMode,
} from "../src/state/transitions.ts";
import { renderQuestionScreen } from "../src/ui/render-question.ts";
function mockTheme(onColor?: (color: string, text: string) => void) {
return {
fg(color: string, text: string) {
onColor?.(color, text);
return onColor ? text : `<${color}>${text}</${color}>`;
},
bg(color: string, text: string) {
return onColor ? text : `{${color}}${text}{/${color}}`;
},
bold(text: string) {
return text;
},
} as never;
}
function mockEditor(text = "", renderedLines?: string[]) {
return {
getText() {
return text;
},
render() {
return renderedLines ?? [];
},
} as never;
}
test("custom option stays labeled before selection", () => {
const state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
});
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor(),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
assert(lines.some((line) => line.includes("Type your own")));
assert(!lines.some((line) => line.includes("Type your answer...")));
});
test("standard options show recommendation metadata without changing answers", () => {
let state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [
{
value: "a",
label: "Option A",
description: "Best fit for the stated constraints",
recommended: true,
},
{ value: "b", label: "Option B" },
],
},
],
});
const lines: string[] = [];
const calls: Array<{ color: string; text: string }> = [];
renderQuestionScreen({
editor: mockEditor(),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme((color, text) => calls.push({ color, text })),
width: 80,
});
assert(lines.some((line) => line.includes("(recommended) | Best fit")));
assert(
calls.some(
(call) => call.color === "warning" && call.text === "(recommended)"
)
);
assert(
calls.some(
(call) =>
call.color === "muted" &&
call.text === " | Best fit for the stated constraints"
)
);
assert.equal(state.answers.q1, undefined);
state = applyNumberShortcut(state, 1);
assert.equal(state.answers.q1.selected[0]?.label, "Option A");
});
test("selected custom option keeps its label and renders editor below", () => {
let state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
});
state = applyNumberShortcut(state, 2);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
assert(lines.some((line) => line.includes("Type your own")));
assert(lines.some((line) => line.includes("Type answer...")));
});
test("open question note renders inline label and editor", () => {
const state = enterQuestionNoteMode(
createInitialState({
questions: [
{
id: "q1",
prompt: "Pick any extra things to include.",
options: [{ value: "a", label: "A" }],
},
],
}),
"q1"
);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
const promptIndex = lines.findIndex((line) =>
line.includes("Pick any extra things to include.")
);
const inputIndex = lines.findIndex((line) => line.includes("Add a note..."));
assert.notEqual(promptIndex, -1);
assert.equal(inputIndex, promptIndex + 1);
assert(!lines.some((line) => line.includes("Note:")));
});
test("open option note renders flush below the option", () => {
const state = enterOptionNoteMode(
createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "Option A" }],
},
],
}),
"q1",
"a"
);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
const optionIndex = lines.findIndex((line) => line.includes("Option A"));
const inputIndex = lines.findIndex((line) => line.includes("Add a note..."));
assert.notEqual(optionIndex, -1);
assert.equal(inputIndex, optionIndex + 1);
});
test("selected multiline custom option renders full editor block", () => {
let state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
});
state = applyNumberShortcut(state, 2);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("first line\nsecond line", [
"┌────┐",
"first line",
"second line",
"└────┘",
]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
assert(lines.some((line) => line.includes("Type your own")));
assert(lines.some((line) => line.includes("first line")));
assert(lines.some((line) => line.includes("second line")));
assert(!lines.some((line) => line.includes("first line second line")));
});
test("freeform-only question renders label without numbering or pointer and separates input", () => {
const state = createInitialState(
{
questions: [
{
id: "q1",
prompt: "Type one",
options: [
{ value: "freeform", label: "Type answer", freeform: true },
],
},
],
},
{ allowFreeform: true }
);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state: { ...state, view: { kind: "input", questionId: "q1" } },
theme: mockTheme(),
width: 80,
});
const labelIndex = lines.findIndex((line) =>
line.includes("Type your answer:")
);
const inputIndex = lines.findIndex((line) => line.includes("Type answer..."));
assert.notEqual(labelIndex, -1);
assert.equal(inputIndex, labelIndex + 2);
assert(lines[labelIndex]?.startsWith(" "));
assert(lines[inputIndex]?.startsWith(" "));
assert(!lines[inputIndex]?.startsWith(" "));
assert(!lines.some((line) => line.includes("1. Type your answer:")));
assert(!lines.some((line) => line.includes("❯")));
});
test("preview questions show custom and recommended options", () => {
const state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
type: "preview",
options: [
{
value: "a",
label: "A",
preview: "Preview A",
recommended: true,
},
],
},
],
});
const lines: string[] = [];
const calls: Array<{ color: string; text: string }> = [];
renderQuestionScreen({
editor: mockEditor(),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme((color, text) => calls.push({ color, text })),
width: 80,
});
assert(lines.some((line) => line.includes("Type your own")));
assert(lines.some((line) => line.trim() === "(recommended)"));
assert(
calls.some(
(call) => call.color === "warning" && call.text === "(recommended)"
)
);
assert(
!calls.some((call) => call.color === "muted" && call.text.startsWith(" |"))
);
assert.equal(state.answers.q1, undefined);
});
test("preview custom option reuses the normal inline editor", () => {
let state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
type: "preview",
options: [{ value: "a", label: "A", preview: "Preview A" }],
},
],
});
state = applyNumberShortcut(state, 2);
const lines: string[] = [];
renderQuestionScreen({
editor: mockEditor("", ["┌────┐", "", "└────┘"]),
lines,
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: mockTheme(),
width: 80,
});
assert(lines.some((line) => line.includes("Type your own")));
assert(lines.some((line) => line.includes("Type answer...")));
const optionIndex = lines.findIndex((line) => line.includes("Type your own"));
const inputIndex = lines.findIndex((line) => line.includes("Type answer..."));
assert.equal(inputIndex, optionIndex + 1);
assert(!lines.some((line) => line.includes("Preview A")));
});