feat(ask): refine submission and custom answer flow
This commit is contained in:
@@ -85,7 +85,7 @@ Use it when the agent needs structured clarification before proceeding.
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
type?: "single" | "multi" | "code-review",
|
type?: "single" | "multi" | "code-review",
|
||||||
allowOther?: boolean,
|
allowOther?: boolean,
|
||||||
required?: boolean,
|
required?: boolean, // metadata only; submit is never blocked
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: string,
|
value: string,
|
||||||
@@ -150,7 +150,7 @@ The current prototype supports:
|
|||||||
- tabbed multi-question flow
|
- tabbed multi-question flow
|
||||||
- single-select questions
|
- single-select questions
|
||||||
- multi-select questions
|
- multi-select questions
|
||||||
- fixed `Type something.` fallback option
|
- fixed `Type your own` fallback option that preserves typed text when dismissed
|
||||||
- number-key quick selection
|
- number-key quick selection
|
||||||
- final submit/review page
|
- final submit/review page
|
||||||
- transcript rendering for call/result rows
|
- transcript rendering for call/result rows
|
||||||
|
|||||||
+35
-40
@@ -8,13 +8,11 @@ import {
|
|||||||
visibleWidth,
|
visibleWidth,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import {
|
import {
|
||||||
allRequiredAnswered,
|
|
||||||
applyNumberShortcut,
|
applyNumberShortcut,
|
||||||
cancelFlow,
|
cancelFlow,
|
||||||
confirmCurrentSelection,
|
confirmCurrentSelection,
|
||||||
createInitialState,
|
createInitialState,
|
||||||
enterInputMode,
|
enterInputMode,
|
||||||
exitInputMode,
|
|
||||||
getAnswer,
|
getAnswer,
|
||||||
getCurrentQuestion,
|
getCurrentQuestion,
|
||||||
getRenderableOptions,
|
getRenderableOptions,
|
||||||
@@ -22,6 +20,7 @@ import {
|
|||||||
isSubmitTab,
|
isSubmitTab,
|
||||||
moveOption,
|
moveOption,
|
||||||
moveTab,
|
moveTab,
|
||||||
|
saveCustomAnswer,
|
||||||
submitCustomAnswer,
|
submitCustomAnswer,
|
||||||
toAskResult,
|
toAskResult,
|
||||||
toggleCurrentMultiOption,
|
toggleCurrentMultiOption,
|
||||||
@@ -35,7 +34,6 @@ export async function runAskFlow(
|
|||||||
return ctx.ui.custom<AskResult>((tui, theme, keybindings, done) => {
|
return ctx.ui.custom<AskResult>((tui, theme, keybindings, done) => {
|
||||||
let state: AskState = createInitialState(params);
|
let state: AskState = createInitialState(params);
|
||||||
let cachedLines: string[] | undefined;
|
let cachedLines: string[] | undefined;
|
||||||
let inputEscapePending = false;
|
|
||||||
let suppressAutoInputForSelection = false;
|
let suppressAutoInputForSelection = false;
|
||||||
const newLineHint = formatKeybindingLabel(
|
const newLineHint = formatKeybindingLabel(
|
||||||
keybindings.getKeys("tui.input.newLine")[0] ?? "shift+enter",
|
keybindings.getKeys("tui.input.newLine")[0] ?? "shift+enter",
|
||||||
@@ -54,7 +52,6 @@ export async function runAskFlow(
|
|||||||
const editor = new Editor(tui, editorTheme);
|
const editor = new Editor(tui, editorTheme);
|
||||||
|
|
||||||
editor.onSubmit = (value) => {
|
editor.onSubmit = (value) => {
|
||||||
inputEscapePending = false;
|
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
state = submitCustomAnswer(state, value);
|
state = submitCustomAnswer(state, value);
|
||||||
editor.setText("");
|
editor.setText("");
|
||||||
@@ -102,7 +99,6 @@ export async function runAskFlow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
state = enterInputMode(state, question.id);
|
state = enterInputMode(state, question.id);
|
||||||
inputEscapePending = false;
|
|
||||||
hydrateEditorForInputMode();
|
hydrateEditorForInputMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,36 +107,27 @@ export async function runAskFlow(
|
|||||||
function handleInput(data: string) {
|
function handleInput(data: string) {
|
||||||
if (state.mode === "input") {
|
if (state.mode === "input") {
|
||||||
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
||||||
state = exitInputMode(state);
|
state = saveCustomAnswer(state, editor.getText());
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveTab(state, 1);
|
state = moveTab(state, 1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
||||||
state = exitInputMode(state);
|
state = saveCustomAnswer(state, editor.getText());
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveTab(state, -1);
|
state = moveTab(state, -1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (matchesKey(data, Key.escape)) {
|
if (matchesKey(data, Key.escape)) {
|
||||||
if (editor.getText().trim().length === 0 || inputEscapePending) {
|
state = saveCustomAnswer(state, editor.getText());
|
||||||
state = exitInputMode(state);
|
suppressAutoInputForSelection = true;
|
||||||
suppressAutoInputForSelection = true;
|
|
||||||
inputEscapePending = false;
|
|
||||||
refresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inputEscapePending = true;
|
|
||||||
refresh();
|
refresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inputEscapePending = false;
|
|
||||||
editor.handleInput(data);
|
editor.handleInput(data);
|
||||||
refresh();
|
refresh();
|
||||||
return;
|
return;
|
||||||
@@ -148,7 +135,6 @@ export async function runAskFlow(
|
|||||||
|
|
||||||
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.right)) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveTab(state, 1);
|
state = moveTab(state, 1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -156,7 +142,6 @@ export async function runAskFlow(
|
|||||||
}
|
}
|
||||||
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
if (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left)) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveTab(state, -1);
|
state = moveTab(state, -1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -164,7 +149,6 @@ export async function runAskFlow(
|
|||||||
}
|
}
|
||||||
if (matchesKey(data, Key.up)) {
|
if (matchesKey(data, Key.up)) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveOption(state, -1);
|
state = moveOption(state, -1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -172,7 +156,6 @@ export async function runAskFlow(
|
|||||||
}
|
}
|
||||||
if (matchesKey(data, Key.down)) {
|
if (matchesKey(data, Key.down)) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = moveOption(state, 1);
|
state = moveOption(state, 1);
|
||||||
syncInputModeWithSelection();
|
syncInputModeWithSelection();
|
||||||
refresh();
|
refresh();
|
||||||
@@ -182,7 +165,6 @@ export async function runAskFlow(
|
|||||||
const question = getCurrentQuestion(state);
|
const question = getCurrentQuestion(state);
|
||||||
if (question?.type === "multi") {
|
if (question?.type === "multi") {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = toggleCurrentMultiOption(state);
|
state = toggleCurrentMultiOption(state);
|
||||||
if (state.mode === "input") {
|
if (state.mode === "input") {
|
||||||
hydrateEditorForInputMode();
|
hydrateEditorForInputMode();
|
||||||
@@ -193,7 +175,6 @@ export async function runAskFlow(
|
|||||||
}
|
}
|
||||||
if (matchesKey(data, Key.enter)) {
|
if (matchesKey(data, Key.enter)) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = confirmCurrentSelection(state);
|
state = confirmCurrentSelection(state);
|
||||||
if (state.mode === "input") {
|
if (state.mode === "input") {
|
||||||
hydrateEditorForInputMode();
|
hydrateEditorForInputMode();
|
||||||
@@ -212,7 +193,6 @@ export async function runAskFlow(
|
|||||||
const digit = parseDigit(data);
|
const digit = parseDigit(data);
|
||||||
if (digit !== null) {
|
if (digit !== null) {
|
||||||
suppressAutoInputForSelection = false;
|
suppressAutoInputForSelection = false;
|
||||||
inputEscapePending = false;
|
|
||||||
state = applyNumberShortcut(state, digit);
|
state = applyNumberShortcut(state, digit);
|
||||||
if (state.mode === "input") {
|
if (state.mode === "input") {
|
||||||
hydrateEditorForInputMode();
|
hydrateEditorForInputMode();
|
||||||
@@ -247,16 +227,23 @@ export async function runAskFlow(
|
|||||||
add();
|
add();
|
||||||
|
|
||||||
const isMulti = question.type === "multi";
|
const isMulti = question.type === "multi";
|
||||||
|
const answer = getAnswer(state, question.id);
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
const option = options[i];
|
const option = options[i];
|
||||||
const selected = i === state.optionIndex;
|
const selected = i === state.optionIndex;
|
||||||
const checked = !!getAnswer(state, question.id)?.values.includes(
|
const isAnsweredOption = option.isOther
|
||||||
option.value,
|
? !!answer?.customText
|
||||||
);
|
: !!answer?.values.includes(option.value);
|
||||||
const pointer = selected ? theme.fg("accent", "❯ ") : " ";
|
const pointer = selected ? theme.fg("accent", "❯ ") : " ";
|
||||||
const prefix =
|
const prefix =
|
||||||
isMulti && !option.isOther ? `[${checked ? "x" : " "}] ` : "";
|
isMulti && !option.isOther
|
||||||
const optionColor = selected ? "accent" : "text";
|
? `[${isAnsweredOption ? "x" : " "}] `
|
||||||
|
: "";
|
||||||
|
const optionColor = isAnsweredOption
|
||||||
|
? "success"
|
||||||
|
: selected
|
||||||
|
? "accent"
|
||||||
|
: "text";
|
||||||
add(
|
add(
|
||||||
`${pointer}${theme.fg(optionColor, `${i + 1}. ${prefix}${option.label}`)}`,
|
`${pointer}${theme.fg(optionColor, `${i + 1}. ${prefix}${option.label}`)}`,
|
||||||
);
|
);
|
||||||
@@ -275,9 +262,17 @@ export async function runAskFlow(
|
|||||||
add(` ${renderInputLine(editorLine, width - 5, theme)}`);
|
add(` ${renderInputLine(editorLine, width - 5, theme)}`);
|
||||||
}
|
}
|
||||||
add(
|
add(
|
||||||
` ${theme.fg("dim", inputEscapePending ? `${newLineHint} newline · Enter submit · Esc again to go back` : `${newLineHint} newline · Enter submit · Esc to go back if empty`)}`,
|
` ${theme.fg("dim", `${newLineHint} newline · Enter submit · Esc save and close`)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (option.isOther && state.mode !== "input") {
|
||||||
|
const customText = answer?.customText;
|
||||||
|
if (customText) {
|
||||||
|
for (const customLine of customText.split("\n")) {
|
||||||
|
add(` ${theme.fg("muted", customLine)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +321,7 @@ function renderTabs(
|
|||||||
segments.push(
|
segments.push(
|
||||||
submitActive
|
submitActive
|
||||||
? theme.bg("selectedBg", theme.fg("text", submitText))
|
? theme.bg("selectedBg", theme.fg("text", submitText))
|
||||||
: theme.fg(allRequiredAnswered(state) ? "success" : "dim", submitText),
|
: theme.fg("success", submitText),
|
||||||
);
|
);
|
||||||
segments.push(theme.fg("dim", " →"));
|
segments.push(theme.fg("dim", " →"));
|
||||||
|
|
||||||
@@ -346,15 +341,18 @@ function renderSubmit(
|
|||||||
for (const question of state.questions) {
|
for (const question of state.questions) {
|
||||||
const answer = getAnswer(state, question.id);
|
const answer = getAnswer(state, question.id);
|
||||||
add(` ${theme.fg("muted", `● ${question.prompt}`)}`);
|
add(` ${theme.fg("muted", `● ${question.prompt}`)}`);
|
||||||
|
if (!answer) {
|
||||||
|
add(` ${theme.fg("warning", "→ (unanswered)")}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const answerText = answer.customText ?? answer.labels.join(", ");
|
||||||
add(
|
add(
|
||||||
` ${theme.fg("text", `→ ${answer ? answer.labels.join(", ") : "(unanswered)"}`)}`,
|
` ${theme.fg(answer.customText ? "text" : "success", `→ ${answerText}`)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
add();
|
add();
|
||||||
add(
|
add(` ${theme.fg("success", "Ready to submit your answers?")}`);
|
||||||
` ${allRequiredAnswered(state) ? theme.fg("success", "Ready to submit your answers?") : theme.fg("warning", "Answer all required questions before submitting.")}`,
|
|
||||||
);
|
|
||||||
add();
|
add();
|
||||||
|
|
||||||
const options = ["Submit answers", "Cancel"];
|
const options = ["Submit answers", "Cancel"];
|
||||||
@@ -385,10 +383,7 @@ function renderFooter(
|
|||||||
" ⇆ tab · ↑↓ select · space toggle · enter continue · esc dismiss · 1-9 quick toggle",
|
" ⇆ tab · ↑↓ select · space toggle · enter continue · esc dismiss · 1-9 quick toggle",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return theme.fg(
|
return theme.fg("dim", " ⇆ tab · ↑↓ select · enter confirm · esc dismiss");
|
||||||
"dim",
|
|
||||||
" ⇆ tab · ↑↓ select · enter confirm · esc dismiss · 1-9 quick select",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDigit(data: string): number | null {
|
function parseDigit(data: string): number | null {
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ export const AskQuestionSchema = Type.Object({
|
|||||||
),
|
),
|
||||||
required: Type.Optional(
|
required: Type.Optional(
|
||||||
Type.Boolean({
|
Type.Boolean({
|
||||||
description: "Whether an answer is required before submit",
|
description: "Optional metadata flag; submit is never blocked",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
options: Type.Array(AskOptionSchema, {
|
options: Type.Array(AskOptionSchema, {
|
||||||
|
|||||||
+28
-25
@@ -82,16 +82,7 @@ export function isQuestionAnswered(
|
|||||||
return !!answer && answer.labels.length > 0;
|
return !!answer && answer.labels.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function allRequiredAnswered(state: AskState): boolean {
|
|
||||||
return state.questions.every(
|
|
||||||
(question) => !question.required || isQuestionAnswered(state, question.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moveTab(state: AskState, delta: number): AskState {
|
export function moveTab(state: AskState, delta: number): AskState {
|
||||||
if (state.questions.length <= 1 && delta !== 0) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
const totalTabs = state.questions.length + 1;
|
const totalTabs = state.questions.length + 1;
|
||||||
const currentTab = (state.currentTab + delta + totalTabs) % totalTabs;
|
const currentTab = (state.currentTab + delta + totalTabs) % totalTabs;
|
||||||
return {
|
return {
|
||||||
@@ -157,10 +148,7 @@ export function confirmCurrentSelection(state: AskState): AskState {
|
|||||||
if (state.submitIndex === 1) {
|
if (state.submitIndex === 1) {
|
||||||
return { ...state, cancelled: true, completed: true };
|
return { ...state, cancelled: true, completed: true };
|
||||||
}
|
}
|
||||||
if (allRequiredAnswered(state)) {
|
return { ...state, completed: true };
|
||||||
return { ...state, completed: true };
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const question = getCurrentQuestion(state);
|
const question = getCurrentQuestion(state);
|
||||||
@@ -210,33 +198,48 @@ export function applyNumberShortcut(state: AskState, digit: number): AskState {
|
|||||||
return advanceToNextTab(nextState);
|
return advanceToNextTab(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitCustomAnswer(
|
export function saveCustomAnswer(state: AskState, rawValue: string): AskState {
|
||||||
state: AskState,
|
|
||||||
rawValue: string,
|
|
||||||
): AskState {
|
|
||||||
const question = state.questions.find(
|
const question = state.questions.find(
|
||||||
(item) => item.id === state.inputQuestionId,
|
(item) => item.id === state.inputQuestionId,
|
||||||
);
|
);
|
||||||
if (!question) return exitInputMode(state);
|
if (!question) return exitInputMode(state);
|
||||||
|
|
||||||
const trimmed = rawValue.trim();
|
const trimmed = rawValue.trim();
|
||||||
|
const nextState = exitInputMode(state);
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return exitInputMode(state);
|
const answers = { ...nextState.answers };
|
||||||
|
delete answers[question.id];
|
||||||
|
return {
|
||||||
|
...nextState,
|
||||||
|
answers,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const answer: AskAnswer = {
|
const answer: AskAnswer = {
|
||||||
values: [trimmed],
|
values: [rawValue],
|
||||||
labels: [trimmed],
|
labels: [rawValue],
|
||||||
indices: [],
|
indices: [],
|
||||||
customText: trimmed,
|
customText: rawValue,
|
||||||
};
|
};
|
||||||
return advanceToNextTab({
|
return {
|
||||||
...exitInputMode(state),
|
...nextState,
|
||||||
answers: {
|
answers: {
|
||||||
...state.answers,
|
...nextState.answers,
|
||||||
[question.id]: answer,
|
[question.id]: answer,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitCustomAnswer(
|
||||||
|
state: AskState,
|
||||||
|
rawValue: string,
|
||||||
|
): AskState {
|
||||||
|
const questionId = state.inputQuestionId;
|
||||||
|
const nextState = saveCustomAnswer(state, rawValue);
|
||||||
|
if (!questionId || !getAnswer(nextState, questionId)) {
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
return advanceToNextTab(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cancelFlow(state: AskState): AskState {
|
export function cancelFlow(state: AskState): AskState {
|
||||||
|
|||||||
+68
-2
@@ -1,11 +1,11 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import {
|
import {
|
||||||
allRequiredAnswered,
|
|
||||||
applyNumberShortcut,
|
applyNumberShortcut,
|
||||||
cancelFlow,
|
cancelFlow,
|
||||||
confirmCurrentSelection,
|
confirmCurrentSelection,
|
||||||
createInitialState,
|
createInitialState,
|
||||||
|
saveCustomAnswer,
|
||||||
getRenderableOptions,
|
getRenderableOptions,
|
||||||
moveOption,
|
moveOption,
|
||||||
moveTab,
|
moveTab,
|
||||||
@@ -84,7 +84,17 @@ test("multi-select space toggles current option and enter advances to submit", (
|
|||||||
state = confirmCurrentSelection(state);
|
state = confirmCurrentSelection(state);
|
||||||
assert.equal(state.mode, "submit");
|
assert.equal(state.mode, "submit");
|
||||||
assert.equal(state.currentTab, 2);
|
assert.equal(state.currentTab, 2);
|
||||||
assert.equal(allRequiredAnswered(state), true);
|
assert.equal(state.completed, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("submit can complete even when some questions are unanswered", () => {
|
||||||
|
let state = createInitialState(sampleParams());
|
||||||
|
state = moveTab(state, -1);
|
||||||
|
assert.equal(state.mode, "submit");
|
||||||
|
state = confirmCurrentSelection(state);
|
||||||
|
|
||||||
|
assert.equal(state.completed, true);
|
||||||
|
assert.equal(state.cancelled, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("selecting other enters input mode and custom submit stores typed answer", () => {
|
test("selecting other enters input mode and custom submit stores typed answer", () => {
|
||||||
@@ -106,6 +116,46 @@ test("selecting other enters input mode and custom submit stores typed answer",
|
|||||||
assert.equal(state.mode, "submit");
|
assert.equal(state.mode, "submit");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("escape saves typed custom answer without advancing", () => {
|
||||||
|
let state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
prompt: "Other?",
|
||||||
|
options: [{ value: "a", label: "A" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
state = applyNumberShortcut(state, 2);
|
||||||
|
assert.equal(state.mode, "input");
|
||||||
|
state = saveCustomAnswer(state, "my draft answer");
|
||||||
|
|
||||||
|
assert.equal(state.mode, "navigate");
|
||||||
|
assert.equal(state.currentTab, 0);
|
||||||
|
assert.equal(state.answers.q1.customText, "my draft answer");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty custom draft clears the stored answer", () => {
|
||||||
|
let state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "q1",
|
||||||
|
prompt: "Other?",
|
||||||
|
options: [{ value: "a", label: "A" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
state = applyNumberShortcut(state, 2);
|
||||||
|
state = saveCustomAnswer(state, "my answer");
|
||||||
|
state = applyNumberShortcut(state, 2);
|
||||||
|
state = saveCustomAnswer(state, " ");
|
||||||
|
|
||||||
|
assert.equal(state.answers.q1, undefined);
|
||||||
|
assert.equal(state.mode, "navigate");
|
||||||
|
});
|
||||||
|
|
||||||
test("tab navigation wraps including submit tab", () => {
|
test("tab navigation wraps including submit tab", () => {
|
||||||
let state = createInitialState(sampleParams());
|
let state = createInitialState(sampleParams());
|
||||||
state = moveTab(state, -1);
|
state = moveTab(state, -1);
|
||||||
@@ -117,6 +167,22 @@ test("tab navigation wraps including submit tab", () => {
|
|||||||
assert.equal(state.mode, "navigate");
|
assert.equal(state.mode, "navigate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("tab navigation works with a single question", () => {
|
||||||
|
let state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{ id: "q", prompt: "Q?", options: [{ value: "a", label: "A" }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
state = moveTab(state, -1);
|
||||||
|
assert.equal(state.currentTab, 1);
|
||||||
|
assert.equal(state.mode, "submit");
|
||||||
|
|
||||||
|
state = moveTab(state, 1);
|
||||||
|
assert.equal(state.currentTab, 0);
|
||||||
|
assert.equal(state.mode, "navigate");
|
||||||
|
});
|
||||||
|
|
||||||
test("escape cancels from main flow but only exits input mode when typing", () => {
|
test("escape cancels from main flow but only exits input mode when typing", () => {
|
||||||
let state = createInitialState(sampleParams());
|
let state = createInitialState(sampleParams());
|
||||||
state = cancelFlow(state);
|
state = cancelFlow(state);
|
||||||
|
|||||||
Reference in New Issue
Block a user