refactor: split ask flow state and view models
This commit is contained in:
@@ -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"
|
||||
);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user