diff --git a/apps/vscode/test/question-dialog.test.ts b/apps/vscode/test/question-dialog.test.ts new file mode 100644 index 0000000000..c7184342df --- /dev/null +++ b/apps/vscode/test/question-dialog.test.ts @@ -0,0 +1,78 @@ +// @vitest-environment jsdom +/// + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { QuestionDialog } from "../webview-ui/src/components/QuestionDialog"; + +const { respondQuestion } = vi.hoisted(() => ({ + respondQuestion: vi.fn(async () => undefined), +})); + +vi.mock("@/stores", () => ({ + useChatStore: () => ({ + pendingQuestion: { + id: "question-request", + tool_call_id: "tool-call", + questions: [ + { + header: "Target", + question: "Choose a target", + options: [{ label: "Tests" }], + }, + { + header: "Scope", + question: "Choose a scope", + options: [{ label: "Focused" }], + }, + ], + }, + respondQuestion, + }), +})); + +describe("QuestionDialog", () => { + let container: HTMLDivElement | undefined; + + afterEach(() => { + container?.remove(); + container = undefined; + respondQuestion.mockClear(); + }); + + it("collects every question before submitting the response", async () => { + const element = document.createElement("div"); + container = element; + document.body.append(element); + const root = createRoot(element); + + await act(async () => { + root.render(React.createElement(QuestionDialog)); + }); + + expect(element.textContent).toContain("Question 1 of 2"); + expect(element.textContent).toContain("Choose a target"); + + await act(async () => { + element.querySelector("button")?.click(); + }); + + expect(respondQuestion).not.toHaveBeenCalled(); + expect(element.textContent).toContain("Question 2 of 2"); + expect(element.textContent).toContain("Choose a scope"); + + await act(async () => { + element.querySelector("button")?.click(); + }); + + expect(respondQuestion).toHaveBeenCalledWith({ + "Choose a target": "Tests", + "Choose a scope": "Focused", + }); + + await act(async () => { + root.unmount(); + }); + }); +}); diff --git a/apps/vscode/tsconfig.json b/apps/vscode/tsconfig.json index bc9d284638..83fa114d1b 100644 --- a/apps/vscode/tsconfig.json +++ b/apps/vscode/tsconfig.json @@ -15,5 +15,11 @@ } }, "include": ["src/**/*", "shared/**/*", "test/**/*"], - "exclude": ["dist", "node_modules", "webview-ui", "test/settings-store.test.ts"] + "exclude": [ + "dist", + "node_modules", + "webview-ui", + "test/question-dialog.test.ts", + "test/settings-store.test.ts" + ] } diff --git a/apps/vscode/webview-ui/src/components/QuestionDialog.tsx b/apps/vscode/webview-ui/src/components/QuestionDialog.tsx index 03a6da1fbc..da69be555d 100644 --- a/apps/vscode/webview-ui/src/components/QuestionDialog.tsx +++ b/apps/vscode/webview-ui/src/components/QuestionDialog.tsx @@ -7,33 +7,40 @@ export function QuestionDialog() { const [customInput, setCustomInput] = useState(""); const [showCustom, setShowCustom] = useState(false); const [selectedIndex, setSelectedIndex] = useState(1); + const [questionIndex, setQuestionIndex] = useState(0); + const [answers, setAnswers] = useState>({}); - // For now, only handle the first question - const question = pendingQuestion?.questions?.[0]; + const question = pendingQuestion?.questions?.[questionIndex]; useEffect(() => { if (pendingQuestion) { setShowCustom(false); setCustomInput(""); setSelectedIndex(1); + setQuestionIndex(0); + setAnswers({}); } }, [pendingQuestion?.id]); if (!pendingQuestion || !question) return null; - const handleSelect = async (optionLabel: string) => { - const answers: Record = { - [question.question]: optionLabel, - }; - await respondQuestion(answers); + const handleAnswer = async (answer: string) => { + const nextAnswers = { ...answers, [question.question]: answer }; + const nextQuestionIndex = questionIndex + 1; + if (nextQuestionIndex < pendingQuestion.questions.length) { + setAnswers(nextAnswers); + setQuestionIndex(nextQuestionIndex); + setShowCustom(false); + setCustomInput(""); + setSelectedIndex(1); + return; + } + await respondQuestion(nextAnswers); }; const handleCustomSubmit = async () => { if (!customInput.trim()) return; - const answers: Record = { - [question.question]: customInput.trim(), - }; - await respondQuestion(answers); + await handleAnswer(customInput.trim()); }; const options = question.options || []; @@ -42,6 +49,11 @@ export function QuestionDialog() { return (
+ {pendingQuestion.questions.length > 1 && ( +
+ Question {questionIndex + 1} of {pendingQuestion.questions.length} +
+ )} {question.header &&
{question.header}
}
{question.question}
@@ -49,7 +61,7 @@ export function QuestionDialog() {