refactor: split ask flow state and view models

This commit is contained in:
Erik Kotsiuba
2026-04-26 02:17:11 +02:00
parent 8ea067dcde
commit 2618d6ea57
21 changed files with 1455 additions and 1173 deletions
+92
View File
@@ -0,0 +1,92 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createInitialState } from "../src/state/create.ts";
import {
getEditorDraft,
saveEditorDraft,
submitEditorDraft,
syncStateToSelection,
} from "../src/state/editor.ts";
import { enterQuestionNoteMode } from "../src/state/transitions.ts";
test("syncStateToSelection opens input when custom option is active", () => {
const state = {
...createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
}),
activeOptionIndex: 1,
};
const next = syncStateToSelection(state);
assert.equal(next.view.kind, "input");
assert.equal(next.view.questionId, "q1");
});
test("saveEditorDraft stores question note drafts without advancing", () => {
const state = enterQuestionNoteMode(
createInitialState({
questions: [
{
id: "q1",
prompt: "Question?",
options: [{ value: "a", label: "A" }],
},
],
}),
"q1"
);
const next = saveEditorDraft(state, "needs examples");
assert.equal(next.view.kind, "navigate");
assert.equal(next.answers.q1.note, "needs examples");
});
test("submitEditorDraft advances submitted custom answers", () => {
const state = syncStateToSelection({
...createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
}),
activeOptionIndex: 1,
});
const next = submitEditorDraft(state, "custom answer");
assert.equal(next.answers.q1.customText, "custom answer");
assert.equal(next.view.kind, "submit");
});
test("getEditorDraft reads saved note and custom text from view", () => {
let state = syncStateToSelection({
...createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
}),
activeOptionIndex: 1,
});
state = saveEditorDraft(state, "draft answer");
state = enterQuestionNoteMode(state, "q1");
state = saveEditorDraft(state, "saved note");
state = enterQuestionNoteMode(state, "q1");
assert.equal(getEditorDraft(state), "saved note");
assert.equal(
getEditorDraft({ ...state, view: { kind: "input", questionId: "q1" } }),
"draft answer"
);
});
+105
View File
@@ -0,0 +1,105 @@
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,
enterQuestionNoteMode,
} from "../src/state/transitions.ts";
import { buildQuestionScreenModel } from "../src/ui/view-models/question.ts";
function buildContext(state: ReturnType<typeof createInitialState>) {
return {
editor: {
getText() {
return "";
},
render() {
return [];
},
} as never,
lines: [],
options: getRenderableOptions(state.questions[0]),
question: state.questions[0],
state,
theme: {
fg(_color: string, text: string) {
return text;
},
bg(_color: string, text: string) {
return text;
},
bold(text: string) {
return text;
},
} as never,
width: 80,
};
}
test("question view model marks active custom option as inline editor", () => {
let state = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
options: [{ value: "a", label: "A" }],
},
],
});
state = applyNumberShortcut(state, 2);
const model = buildQuestionScreenModel(buildContext(state));
assert.equal(model.mode, "standard");
assert.equal(model.rows[1]?.detail?.kind, "editor");
assert.equal(model.rows[1]?.detail?.placeholder, "Type answer...");
});
test("question view model exposes saved question notes", () => {
let state = enterQuestionNoteMode(
createInitialState({
questions: [
{
id: "q1",
prompt: "Question?",
options: [{ value: "a", label: "A" }],
},
],
}),
"q1"
);
state = {
...state,
view: { kind: "navigate" },
answers: { q1: { selected: [], note: "saved" } },
};
const model = buildQuestionScreenModel(buildContext(state));
assert.deepEqual(model.questionNote, { kind: "saved", text: "saved" });
});
test("preview view model selects stacked vs custom layouts", () => {
const base = createInitialState({
questions: [
{
id: "q1",
prompt: "Pick one",
type: "preview",
options: [{ value: "a", label: "A", preview: "Preview A" }],
},
],
});
const stacked = buildQuestionScreenModel(buildContext(base));
assert.equal(stacked.mode, "preview");
assert.equal(stacked.previewLayout, "stacked");
const custom = buildQuestionScreenModel(
buildContext({
...base,
activeOptionIndex: 1,
view: { kind: "input", questionId: "q1" },
})
);
assert.equal(custom.previewLayout, "custom");
});