diff --git a/frontend/node_modules b/frontend/node_modules deleted file mode 120000 index f7f2174ec1..0000000000 --- a/frontend/node_modules +++ /dev/null @@ -1 +0,0 @@ -/Users/psudokit/.ao/data/worktrees/ao/ao-5/frontend/node_modules \ No newline at end of file diff --git a/frontend/src/renderer/components/GlobalSettingsForm.test.tsx b/frontend/src/renderer/components/GlobalSettingsForm.test.tsx index 86b30a8dd4..2aaa0db637 100644 --- a/frontend/src/renderer/components/GlobalSettingsForm.test.tsx +++ b/frontend/src/renderer/components/GlobalSettingsForm.test.tsx @@ -280,6 +280,7 @@ describe("GlobalSettingsForm", () => { await user.click(await screen.findByRole("button", { name: "Report a problem" })); expect(await screen.findByRole("dialog", { name: "Report a problem" })).toBeInTheDocument(); await user.type(screen.getByLabelText("Title"), "Need help with setup"); + await user.type(screen.getByLabelText("What happened?"), "The setup flow stalls after the first prompt."); await user.click(screen.getByRole("radio", { name: "Discord" })); expect(screen.getByRole("button", { name: /copy & open discord/i })).toBeInTheDocument(); @@ -288,6 +289,8 @@ describe("GlobalSettingsForm", () => { await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1)); expect(writeText.mock.calls[0][0]).toContain("**AO feedback**"); expect(screen.getByText("Discord draft copied.")).toBeInTheDocument(); + expect(screen.getByLabelText("Title")).toHaveValue(""); + expect(screen.getByLabelText("What happened?")).toHaveValue(""); await user.click(screen.getByRole("radio", { name: "Email" })); expect(screen.getByRole("button", { name: /copy & open email/i })).toBeInTheDocument(); @@ -295,6 +298,7 @@ describe("GlobalSettingsForm", () => { expect(screen.queryByText("Discord draft copied.")).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: /copy & open email/i })).toBeDisabled(); await user.type(screen.getByLabelText("Title"), "Need help with setup"); + await user.type(screen.getByLabelText("What happened?"), "The setup flow stalls after the first prompt."); await user.click(screen.getByRole("button", { name: /copy & open email/i })); await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2)); diff --git a/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx b/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx new file mode 100644 index 0000000000..5ef22934ed --- /dev/null +++ b/frontend/src/renderer/components/settings/ReportProblemDialog.test.tsx @@ -0,0 +1,37 @@ +/** + * @vitest-environment jsdom + */ +import { act } from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { ReportProblemDialog } from "./ReportProblemDialog"; + +describe("ReportProblemDialog", () => { + it("disables primary action when fields are empty or whitespace-only, and enables when both summary and details contain non-whitespace text", async () => { + await act(async () => { + render(); + await Promise.resolve(); + }); + + const summaryInput = screen.getByPlaceholderText(/brief title/i); + const detailsInput = screen.getByPlaceholderText(/share what happened/i); + const submitButton = screen.getByRole("button", { name: /copy & create github issue/i }); + + expect(submitButton).toBeDisabled(); + + fireEvent.change(summaryInput, { target: { value: "Test Summary" } }); + expect(submitButton).toBeDisabled(); + + fireEvent.change(summaryInput, { target: { value: " " } }); + fireEvent.change(detailsInput, { target: { value: "Test Details" } }); + expect(submitButton).toBeDisabled(); + + fireEvent.change(summaryInput, { target: { value: "Test Summary" } }); + fireEvent.change(detailsInput, { target: { value: " " } }); + expect(submitButton).toBeDisabled(); + + fireEvent.change(summaryInput, { target: { value: "Test Summary" } }); + fireEvent.change(detailsInput, { target: { value: "Test Details" } }); + expect(submitButton).toBeEnabled(); + }); +}); diff --git a/frontend/src/renderer/components/settings/ReportProblemDialog.tsx b/frontend/src/renderer/components/settings/ReportProblemDialog.tsx index d62afdf8af..a1ad54c7d2 100644 --- a/frontend/src/renderer/components/settings/ReportProblemDialog.tsx +++ b/frontend/src/renderer/components/settings/ReportProblemDialog.tsx @@ -59,10 +59,10 @@ const DESTINATIONS: { action: string; icon: (props: DestinationIconProps) => ReactNode; }[] = [ - { value: "github", label: "GitHub", action: "Copy & Create GitHub Issue", icon: GithubIcon }, - { value: "discord", label: "Discord", action: "Copy & Open Discord", icon: DiscordIcon }, - { value: "email", label: "Email", action: "Copy & Open Email", icon: EmailIcon }, -]; + { value: "github", label: "GitHub", action: "Copy & Create GitHub Issue", icon: GithubIcon }, + { value: "discord", label: "Discord", action: "Copy & Open Discord", icon: DiscordIcon }, + { value: "email", label: "Email", action: "Copy & Open Email", icon: EmailIcon }, + ]; export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogProps) { const titleId = useId(); @@ -98,7 +98,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP const input = { summary, details }; const draft = formatReportProblemDraft(input, diagnostics, selectedOutput); const destination = DESTINATIONS.find((option) => option.value === selectedOutput) ?? DESTINATIONS[0]; - const canCopy = summary.trim().length > 0; + const canSubmit = summary.trim().length > 0 && details.trim().length > 0; const clearStatus = () => { setCopiedOutput(null); @@ -106,7 +106,7 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP }; const copyDraft = async () => { - if (!canCopy) return; + if (!canSubmit) return; setCopyError(null); const output = selectedOutput; try { @@ -230,9 +230,12 @@ export function ReportProblemDialog({ open, onOpenChange }: ReportProblemDialogP