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();});
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();
});