import { describe, expect, it } from "vite-plus/test"; import { buildPendingUserInputAnswers, countAnsweredPendingUserInputQuestions, derivePendingUserInputProgress, resolvePendingUserInputAnswer, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, } from "./pendingUserInput"; const singleSelectQuestion = { id: "scope", header: "Scope", question: "What should the plan target first?", options: [ { label: "Orchestration-first", description: "Focus on orchestration first", }, ], multiSelect: false, } as const; const multiSelectQuestion = { id: "areas", header: "Areas", question: "Which areas should this change cover?", options: [ { label: "Server", description: "Server", }, { label: "Web", description: "Web", }, ], multiSelect: true, } as const; const nativeChoiceQuestion = { id: "result", header: "Result", question: "Which result should be used?", options: [ { value: " first\t", label: "Result", description: "First result" }, { value: "second", label: "Result", description: "Second result" }, ], allowCustomAnswer: false, multiSelect: false, } as const; describe("resolvePendingUserInputAnswer", () => { it("prefers a custom answer over selected options", () => { expect( resolvePendingUserInputAnswer(singleSelectQuestion, { selectedOptionValues: ["Orchestration-first"], customAnswer: "Keep the existing envelope for one release", }), ).toBe("Keep the existing envelope for one release"); }); it("falls back to the selected option for single-select questions", () => { expect( resolvePendingUserInputAnswer(singleSelectQuestion, { selectedOptionValues: ["Orchestration-first"], }), ).toBe("Orchestration-first"); }); it("returns all selected labels for multi-select questions", () => { expect( resolvePendingUserInputAnswer(multiSelectQuestion, { selectedOptionValues: ["Server", "Web"], }), ).toEqual(["Server", "Web"]); }); it("clears the preset selection when a custom answer is entered", () => { expect( setPendingUserInputCustomAnswer( { selectedOptionValues: ["Server", "Web"], }, "doesn't matter", ), ).toEqual({ customAnswer: "doesn't matter", }); }); it("does not replace a required choice with a custom answer", () => { expect( resolvePendingUserInputAnswer(nativeChoiceQuestion, { selectedOptionValues: ["second"], customAnswer: "Use another result", }), ).toBe("second"); }); it("does not submit labels or unknown values when an option has a value", () => { expect( resolvePendingUserInputAnswer(nativeChoiceQuestion, { selectedOptionValues: ["Result", "unknown"], }), ).toBeNull(); }); }); describe("togglePendingUserInputOptionSelection", () => { it("toggles options for multi-select questions", () => { expect(togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Server")).toEqual( { customAnswer: "", selectedOptionValues: ["Server"], }, ); expect( togglePendingUserInputOptionSelection( multiSelectQuestion, { selectedOptionValues: ["Server", "Web"], }, "Server", ), ).toEqual({ customAnswer: "", selectedOptionValues: ["Web"], }); }); it("selects and removes options with the same label independently", () => { const question = { ...nativeChoiceQuestion, multiSelect: true }; const firstSelected = togglePendingUserInputOptionSelection(question, undefined, " first\t"); const bothSelected = togglePendingUserInputOptionSelection(question, firstSelected, "second"); expect(buildPendingUserInputAnswers([question], { result: bothSelected })).toEqual({ result: [" first\t", "second"], }); const secondSelected = togglePendingUserInputOptionSelection( question, bothSelected, " first\t", ); expect(buildPendingUserInputAnswers([question], { result: secondSelected })).toEqual({ result: ["second"], }); }); }); describe("buildPendingUserInputAnswers", () => { it("returns a canonical answer map for complete prompts", () => { expect( buildPendingUserInputAnswers( [ singleSelectQuestion, { id: "compat", header: "Compat", question: "How strict should compatibility be?", options: [ { label: "Keep current envelope", description: "Preserve current wire format", }, ], multiSelect: false, }, ], { scope: { selectedOptionValues: ["Orchestration-first"], }, compat: { customAnswer: "Keep the current envelope for one release window", }, }, ), ).toEqual({ scope: "Orchestration-first", compat: "Keep the current envelope for one release window", }); }); it("returns arrays for answered multi-select prompts", () => { expect( buildPendingUserInputAnswers([multiSelectQuestion], { areas: { selectedOptionValues: ["Server", "Web"], }, }), ).toEqual({ areas: ["Server", "Web"], }); }); it("returns null when any question is unanswered", () => { expect(buildPendingUserInputAnswers([singleSelectQuestion], {})).toBeNull(); }); it.each([" first\t", ""])("preserves the exact selected option value %j", (value) => { const question = { ...nativeChoiceQuestion, options: [{ ...nativeChoiceQuestion.options[0], value }, nativeChoiceQuestion.options[1]], }; const draft = togglePendingUserInputOptionSelection(question, undefined, value); expect(buildPendingUserInputAnswers([question], { result: draft })).toEqual({ result: value }); expect(derivePendingUserInputProgress([question], { result: draft }, 0)).toMatchObject({ selectedOptionValues: [value], answeredQuestionCount: 1, canAdvance: true, isComplete: true, }); }); }); describe("pending user input question progress", () => { const questions = [ singleSelectQuestion, { id: "compat", header: "Compat", question: "How strict should compatibility be?", options: [ { label: "Keep current envelope", description: "Preserve current wire format", }, ], multiSelect: false, }, ] as const; it("counts only answered questions", () => { expect( countAnsweredPendingUserInputQuestions(questions, { scope: { selectedOptionValues: ["Orchestration-first"], }, }), ).toBe(1); }); it("derives the active question and advancement state", () => { expect( derivePendingUserInputProgress( questions, { scope: { selectedOptionValues: ["Orchestration-first"], }, }, 0, ), ).toMatchObject({ questionIndex: 0, activeQuestion: questions[0], selectedOptionValues: ["Orchestration-first"], customAnswer: "", resolvedAnswer: "Orchestration-first", answeredQuestionCount: 1, isLastQuestion: false, isComplete: false, canAdvance: true, }); }); it("treats multi-select questions as answered when they have selected options", () => { expect( derivePendingUserInputProgress( [multiSelectQuestion], { areas: { selectedOptionValues: ["Server", "Web"], }, }, 0, ), ).toMatchObject({ selectedOptionValues: ["Server", "Web"], resolvedAnswer: ["Server", "Web"], canAdvance: true, isComplete: true, }); }); it("requires an option when custom answers are disabled", () => { const drafts = { result: { customAnswer: "Use another result" } }; expect(buildPendingUserInputAnswers([nativeChoiceQuestion], drafts)).toBeNull(); expect(derivePendingUserInputProgress([nativeChoiceQuestion], drafts, 0)).toMatchObject({ customAnswer: "", usingCustomAnswer: false, resolvedAnswer: null, answeredQuestionCount: 0, canAdvance: false, isComplete: false, }); }); }); it("accepts attachment-only answers after every upload finishes", () => { const questions = [ { id: "spec", header: "Spec", question: "Provide a spec", options: [], multiSelect: false }, ]; expect(buildPendingUserInputAnswers(questions, { spec: { attachmentCount: 1 } })).toEqual({ spec: "", }); expect( buildPendingUserInputAnswers(questions, { spec: { attachmentCount: 1, attachmentsBlocked: true }, }), ).toBeNull(); expect( buildPendingUserInputAnswers([{ ...questions[0]!, allowCustomAnswer: false }], { spec: { attachmentCount: 1 }, }), ).toBeNull(); });