Skip to content
Source
Sign inSign up

pendingUserInput.test.ts

207 lines7.0 KB

1import { describe, expect, it } from "vite-plus/test";
2​
3import {
4 buildPendingUserInputAnswers,
5 isPendingUserInputOptionSelected,
6 resolvePendingUserInputAnswer,
7 setPendingUserInputCustomAnswer,
8 togglePendingUserInputOptionSelection,
9} from "./pendingUserInput.ts";
10​
11const singleSelectQuestion = {
12 id: "scope",
13 header: "Scope",
14 question: "What should the plan target first?",
15 options: [
16 { label: "Orchestration-first", description: "Focus on orchestration first" },
17 { label: "Node.js", description: "Reuse TypeScript" },
18 ],
19 multiSelect: false,
20} as const;
21​
22const nativeChoiceQuestion = {
23 id: "result",
24 header: "Result",
25 question: "Which result should be used?",
26 options: [
27 { value: " first\t", label: "Result", description: "First result" },
28 { value: "second", label: "Result", description: "Second result" },
29 ],
30 allowCustomAnswer: false,
31 multiSelect: false,
32} as const;
33​
34describe("resolvePendingUserInputAnswer", () => {
35 it("preserves exact editor text, including a deliberately cleared answer", () => {
36 const question = { ...singleSelectQuestion, initialAnswer: " Proposed message\n" };
37 expect(resolvePendingUserInputAnswer(question, { customAnswer: question.initialAnswer })).toBe(
38 question.initialAnswer,
39 );
40 expect(resolvePendingUserInputAnswer(question, { customAnswer: " Edited message\n\n" })).toBe(
41 " Edited message\n\n",
42 );
43 expect(resolvePendingUserInputAnswer(question, { customAnswer: "" })).toBe("");
44 expect(resolvePendingUserInputAnswer(question, { customAnswer: " \n" })).toBe(" \n");
45 expect(
46 setPendingUserInputCustomAnswer(
47 question,
48 { selectedOptionValues: ["Orchestration-first"] },
49 "",
50 ),
51 ).toEqual({ customAnswer: "" });
52 });
53​
54 it("prefers a custom answer over selected options", () => {
55 expect(
56 resolvePendingUserInputAnswer(singleSelectQuestion, {
57 selectedOptionValues: ["Orchestration-first"],
58 customAnswer: "Keep the existing envelope for one release",
59 }),
60 ).toBe("Keep the existing envelope for one release");
61 });
62​
63 it("does not replace a required choice with a custom answer", () => {
64 expect(
65 resolvePendingUserInputAnswer(nativeChoiceQuestion, {
66 selectedOptionValues: ["second"],
67 customAnswer: "Use another result",
68 }),
69 ).toBe("second");
70 });
71​
72 it("does not submit labels or unknown values when an option has a value", () => {
73 expect(
74 resolvePendingUserInputAnswer(nativeChoiceQuestion, {
75 selectedOptionValues: ["Result", "unknown"],
76 }),
77 ).toBeNull();
78 });
79​
80 it("matches a label only for an option without a value", () => {
81 expect(
82 resolvePendingUserInputAnswer(singleSelectQuestion, { selectedOptionValues: [" Node.js "] }),
83 ).toBe("Node.js");
84 });
85​
86 it("accepts ready attachment-only answers while preserving selected options", () => {
87 expect(resolvePendingUserInputAnswer(singleSelectQuestion, { attachmentCount: 1 })).toBe("");
88 expect(
89 resolvePendingUserInputAnswer(singleSelectQuestion, {
90 attachmentCount: 1,
91 selectedOptionValues: ["Node.js"],
92 }),
93 ).toBe("Node.js");
94 expect(
95 resolvePendingUserInputAnswer(singleSelectQuestion, {
96 attachmentCount: 1,
97 attachmentsBlocked: true,
98 }),
99 ).toBeNull();
100 expect(
101 resolvePendingUserInputAnswer(
102 { ...singleSelectQuestion, allowCustomAnswer: false },
103 { attachmentCount: 1 },
104 ),
105 ).toBeNull();
106 });
107});
108​
109describe("togglePendingUserInputOptionSelection", () => {
110 it("selects and removes options with the same label independently", () => {
111 const question = { ...nativeChoiceQuestion, multiSelect: true };
112 const firstSelected = togglePendingUserInputOptionSelection(question, undefined, " first\t");
113 const bothSelected = togglePendingUserInputOptionSelection(question, firstSelected, "second");
114​
115 expect(buildPendingUserInputAnswers([question], { result: bothSelected })).toEqual({
116 result: [" first\t", "second"],
117 });
118​
119 const secondSelected = togglePendingUserInputOptionSelection(
120 question,
121 bothSelected,
122 " first\t",
123 );
124 expect(buildPendingUserInputAnswers([question], { result: secondSelected })).toEqual({
125 result: ["second"],
126 });
127 });
128​
129 it("submits raw option values and leaves the draft alone for an unknown option", () => {
130 const first = togglePendingUserInputOptionSelection(
131 nativeChoiceQuestion,
132 undefined,
133 " first\t",
134 );
135 expect(isPendingUserInputOptionSelected(nativeChoiceQuestion, first, " first\t")).toBe(true);
136 expect(isPendingUserInputOptionSelected(nativeChoiceQuestion, first, "second")).toBe(false);
137 expect(buildPendingUserInputAnswers([nativeChoiceQuestion], { result: first })).toEqual({
138 result: " first\t",
139 });
140 expect(togglePendingUserInputOptionSelection(nativeChoiceQuestion, first, "Result")).toBe(
141 first,
142 );
143 });
144});
145​
146describe("setPendingUserInputCustomAnswer", () => {
147 it("rejects arbitrary text when the provider only accepts offered options", () => {
148 expect(setPendingUserInputCustomAnswer(nativeChoiceQuestion, undefined, "Other")).toEqual({});
149 expect(
150 buildPendingUserInputAnswers([nativeChoiceQuestion], { result: { customAnswer: "Other" } }),
151 ).toBeNull();
152 expect(
153 buildPendingUserInputAnswers([nativeChoiceQuestion], {
154 result: { selectedOptionValues: ["second"], customAnswer: "stale draft" },
155 }),
156 ).toEqual({ result: "second" });
157 });
158});
159​
160const compatQuestion = {
161 id: "compat",
162 header: "Compat",
163 question: "How strict should compatibility be?",
164 options: [{ label: "Keep current envelope", description: "Preserve current wire format" }],
165 multiSelect: false,
166} as const;
167​
168describe("buildPendingUserInputAnswers", () => {
169 it("returns a canonical answer map for complete prompts", () => {
170 expect(
171 buildPendingUserInputAnswers([singleSelectQuestion, compatQuestion], {
172 scope: { selectedOptionValues: ["Orchestration-first"] },
173 compat: { customAnswer: "Keep the current envelope for one release window" },
174 }),
175 ).toEqual({
176 scope: "Orchestration-first",
177 compat: "Keep the current envelope for one release window",
178 });
179 });
180});
181​
182it.each([
183 ["single-select", singleSelectQuestion, ["Orchestration-first"], '"Orchestration-first"'],
184 [
185 "multi-select",
186 {
187 ...singleSelectQuestion,
188 multiSelect: true,
189 options: [{ label: "Server" }, { label: "Web" }],
190 },
191 ["Server", "Web"],
192 '["Server","Web"]',
193 ],
194])("sends a %s answer to a question with the id __proto__", (_kind, question, selected, json) => {
195 const answers = buildPendingUserInputAnswers(
196 [
197 { ...question, id: "__proto__" },
198 { ...singleSelectQuestion, id: "name" },
199 ],
200 {
201 ["__proto__"]: { selectedOptionValues: selected },
202 name: { selectedOptionValues: ["Orchestration-first"] },
203 },
204 );
205​
206 expect(JSON.stringify(answers)).toBe(`{"__proto__":${json},"name":"Orchestration-first"}`);
207});