feat(ask): refine inline answer and navigation UX
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { applyNumberShortcut, createInitialState, enterQuestionNoteMode } from "../src/state.ts";
|
||||
import { getInputCommand } from "../src/ui/input.ts";
|
||||
|
||||
function inputState() {
|
||||
let state = createInitialState({
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
prompt: "Question?",
|
||||
options: [{ value: "a", label: "A" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
state = applyNumberShortcut(state, 2);
|
||||
return state;
|
||||
}
|
||||
|
||||
test("editing views let up and down move navigation", () => {
|
||||
const input = inputState();
|
||||
assert.deepEqual(getInputCommand(input, "\x1b[A"), {
|
||||
kind: "editMoveOption",
|
||||
delta: -1,
|
||||
});
|
||||
assert.deepEqual(getInputCommand(input, "\x1b[B"), {
|
||||
kind: "editMoveOption",
|
||||
delta: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test("note editing views let up and down move navigation", () => {
|
||||
const state = enterQuestionNoteMode(createInitialState({
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
prompt: "Question?",
|
||||
options: [{ value: "a", label: "A" }],
|
||||
},
|
||||
],
|
||||
}), "q1");
|
||||
|
||||
assert.deepEqual(getInputCommand(state, "\x1b[A"), {
|
||||
kind: "editMoveOption",
|
||||
delta: -1,
|
||||
});
|
||||
assert.deepEqual(getInputCommand(state, "\x1b[B"), {
|
||||
kind: "editMoveOption",
|
||||
delta: 1,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { renderEditorBlock } from "../src/ui/render-helpers.ts";
|
||||
|
||||
function mockTheme() {
|
||||
return {
|
||||
fg(color: string, text: string) {
|
||||
return `<${color}>${text}</${color}>`;
|
||||
},
|
||||
bg(color: string, text: string) {
|
||||
return `{${color}}${text}{/${color}}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("renderEditorBlock strips editor borders", () => {
|
||||
const lines: string[] = [];
|
||||
renderEditorBlock({
|
||||
lines,
|
||||
editorLines: ["┌──┐", "│abc│", "└──┘"],
|
||||
width: 40,
|
||||
theme: mockTheme() as never,
|
||||
indent: " ",
|
||||
availableWidth: 40,
|
||||
});
|
||||
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0], /abc/);
|
||||
assert.doesNotMatch(lines[0], /┌|└/);
|
||||
});
|
||||
|
||||
test("renderEditorBlock shows a muted placeholder when empty", () => {
|
||||
const lines: string[] = [];
|
||||
renderEditorBlock({
|
||||
lines,
|
||||
editorLines: ["┌──┐", "│ │", "└──┘"],
|
||||
width: 40,
|
||||
theme: mockTheme() as never,
|
||||
indent: " ",
|
||||
availableWidth: 40,
|
||||
placeholder: "Type your",
|
||||
isEmpty: true,
|
||||
});
|
||||
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0], /Type your/);
|
||||
assert.doesNotMatch(lines[0], /┌|└/);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { applyNumberShortcut, createInitialState } from "../src/state.ts";
|
||||
import { getRenderableOptions } from "../src/state/selectors.ts";
|
||||
import { renderQuestionScreen } from "../src/ui/render-question.ts";
|
||||
|
||||
function mockTheme() {
|
||||
return {
|
||||
fg(color: string, text: string) {
|
||||
return `<${color}>${text}</${color}>`;
|
||||
},
|
||||
bg(color: string, text: string) {
|
||||
return `{${color}}${text}{/${color}}`;
|
||||
},
|
||||
bold(text: string) {
|
||||
return text;
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
function mockEditor(text = "") {
|
||||
return {
|
||||
getText() {
|
||||
return text;
|
||||
},
|
||||
render() {
|
||||
return [];
|
||||
},
|
||||
} 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({
|
||||
lines,
|
||||
state,
|
||||
question: state.questions[0],
|
||||
options: getRenderableOptions(state.questions[0]),
|
||||
theme: mockTheme(),
|
||||
width: 80,
|
||||
editor: mockEditor(),
|
||||
});
|
||||
|
||||
assert(lines.some((line) => line.includes("Type your own")));
|
||||
assert(!lines.some((line) => line.includes("Type your answer...")));
|
||||
});
|
||||
|
||||
test("selected custom option becomes inline input", () => {
|
||||
let state = createInitialState({
|
||||
questions: [
|
||||
{
|
||||
id: "q1",
|
||||
prompt: "Pick one",
|
||||
options: [{ value: "a", label: "A" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
state = applyNumberShortcut(state, 2);
|
||||
|
||||
const lines: string[] = [];
|
||||
renderQuestionScreen({
|
||||
lines,
|
||||
state,
|
||||
question: state.questions[0],
|
||||
options: getRenderableOptions(state.questions[0]),
|
||||
theme: mockTheme(),
|
||||
width: 80,
|
||||
editor: mockEditor(""),
|
||||
});
|
||||
|
||||
assert(lines.some((line) => line.includes("Type your answer...")));
|
||||
assert(!lines.some((line) => line.includes("Type your own answer")));
|
||||
});
|
||||
@@ -122,6 +122,20 @@ test("multi-select space toggles current option and enter advances to submit", (
|
||||
assert.equal(state.completed, false);
|
||||
});
|
||||
|
||||
test("single-select space toggles the active option without advancing", () => {
|
||||
let state = createInitialState(sampleParams());
|
||||
state = toggleCurrentMultiOption(state);
|
||||
|
||||
assert.equal(state.answers.lang.selected[0].label, "Python");
|
||||
assert.equal(state.activeTabIndex, 0);
|
||||
assert.equal(state.view.kind, "navigate");
|
||||
|
||||
state = toggleCurrentMultiOption(state);
|
||||
assert.equal(state.answers.lang, undefined);
|
||||
assert.equal(state.activeTabIndex, 0);
|
||||
assert.equal(state.view.kind, "navigate");
|
||||
});
|
||||
|
||||
test("submit can complete even when some questions are unanswered", () => {
|
||||
let state = createInitialState(sampleParams());
|
||||
state = moveTab(state, -1);
|
||||
|
||||
Reference in New Issue
Block a user