Capture optional report context while preserving scrubbed text-first filing.\n\n- Add browser screenshot capture, review, validation, and compensating upload cleanup.\n- Include bounded activity traces in report drafts and filing flows.\n- Preserve roadmap deduplication and document report privacy behavior.\n\nFiles changed:\n .changeset/fn-8317-report-context.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/api/report.ts | 15 +- packages/dashboard/app/components/ReportModal.css | 5 +- packages/dashboard/app/components/ReportModal.tsx | 52 +++---- .../app/components/__tests__/ReportModal.test.tsx | 14 ++ packages/dashboard/app/hooks/useViewState.ts | 7 + .../app/utils/__tests__/report-capture.test.ts | 17 +++ packages/dashboard/app/utils/report-capture.ts | 47 +++++++ packages/dashboard/package.json | 1 - .../src/__tests__/report-pipeline.test.ts | 100 +++++++++++++- .../dashboard/src/__tests__/report-routes.test.ts | 17 +++ .../dashboard/src/__tests__/report-scrub.test.ts | 8 ++ packages/dashboard/src/github.ts | 107 +++++++++++++++ packages/dashboard/src/report-pipeline.ts | 152 ++++++++++++++++----- packages/dashboard/src/report-scrub.ts | 8 ++ .../dashboard/src/routes/register-report-routes.ts | 103 ++++++-------- pnpm-lock.yaml | 129 +++++++++-------- 18 files changed, 590 insertions(+), 203 deletions(-) Fusion-Task-Id: FN-8317 Fusion-Task-Lineage: 9d200cee-cf69-4827-9e1e-f7789adf09e0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
123 lines
6.9 KiB
TypeScript
123 lines
6.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ReportModal } from "../ReportModal";
|
|
import * as api from "../../api";
|
|
|
|
vi.mock("../../api", () => ({
|
|
reportDraft: vi.fn(),
|
|
reportFile: vi.fn(),
|
|
reportHelp: vi.fn(),
|
|
}));
|
|
|
|
const reportDraft = vi.mocked(api.reportDraft);
|
|
const reportFile = vi.mocked(api.reportFile);
|
|
|
|
describe("ReportModal", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("shows an actionable error and retry affordance when preparing a report fails", async () => {
|
|
reportDraft.mockRejectedValueOnce(new Error("offline"));
|
|
render(<ReportModal actionType="bug" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What went wrong?"), { target: { value: "It crashes" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("could not prepare");
|
|
expect(screen.getByRole("button", { name: "Retry" })).toBeEnabled();
|
|
|
|
reportDraft.mockResolvedValueOnce({ kind: "draft-ready", report: { userPrompt: "It crashes", body: "## Summary\nIt crashes", context: {} } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
|
await waitFor(() => expect(reportDraft).toHaveBeenCalledTimes(2));
|
|
});
|
|
|
|
it("preserves an editable draft and makes filing failures retryable", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "draft-ready", report: { userPrompt: "It crashes", body: "## Summary\nIt crashes", context: {} } });
|
|
reportFile.mockRejectedValueOnce(new Error("offline"));
|
|
render(<ReportModal actionType="bug" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What went wrong?"), { target: { value: "It crashes" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
await screen.findByText("Review your report");
|
|
fireEvent.click(screen.getByRole("button", { name: "File report" }));
|
|
|
|
expect(await screen.findByRole("alert")).toHaveTextContent("draft is still here");
|
|
expect(screen.getByRole("button", { name: "File report" })).toBeEnabled();
|
|
});
|
|
|
|
it("shows an editable draft and requires confirmation before endorsing a duplicate", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "duplicate-found", issue: { number: 22, url: "https://example.test/22", title: "Existing issue" }, report: { userPrompt: "It crashes", body: "## Summary\nIt crashes", context: {} } });
|
|
reportFile.mockResolvedValueOnce({ kind: "endorsed", url: "https://example.test/comments/1" });
|
|
render(<ReportModal actionType="bug" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What went wrong?"), { target: { value: "It crashes" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
await screen.findByText("Review data point for a similar open issue");
|
|
expect(reportFile).not.toHaveBeenCalled();
|
|
fireEvent.change(screen.getByLabelText("Report summary"), { target: { value: "It crashes after saving" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Confirm and add data point" }));
|
|
|
|
await waitFor(() => expect(reportFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
endorseIssueNumber: 22,
|
|
report: expect.objectContaining({ userPrompt: "It crashes after saving" }),
|
|
})));
|
|
});
|
|
|
|
it("shows a roadmap match inline without a filing or dead-link affordance", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "roadmap-match", roadmap: { featureId: "RF-1", title: "Offline report queue", description: "Keep reports available while offline" } });
|
|
render(<ReportModal actionType="idea" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What would you like Fusion to do?"), { target: { value: "Keep reports while offline" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
expect(await screen.findByText("Already on the roadmap")).toBeInTheDocument();
|
|
expect(screen.getByText("Offline report queue")).toBeInTheDocument();
|
|
expect(screen.getByText("Keep reports available while offline")).toBeInTheDocument();
|
|
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /file/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("lets people return to the guided prompt after an unavailable response", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "unavailable", message: "GitHub is not connected" });
|
|
render(<ReportModal actionType="feedback" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What would you like to share?"), { target: { value: "A thought" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
await screen.findByRole("alert");
|
|
fireEvent.click(screen.getByRole("button", { name: "Return to prompt" }));
|
|
expect(screen.getByLabelText("What would you like to share?")).toBeInTheDocument();
|
|
});
|
|
|
|
it("warns when a reviewed screenshot could not be attached", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "draft-ready", report: { userPrompt: "It crashes", body: "## Summary\nIt crashes", context: {} } });
|
|
reportFile.mockResolvedValueOnce({ kind: "filed", url: "https://example.test/1", screenshotNotAttached: true });
|
|
render(<ReportModal actionType="bug" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What went wrong?"), { target: { value: "It crashes" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
await screen.findByText("Review your report");
|
|
fireEvent.click(screen.getByRole("button", { name: "File report" }));
|
|
|
|
expect(await screen.findByText("Report sent without screenshot")).toBeInTheDocument();
|
|
expect(screen.getByRole("status")).toHaveTextContent("No screenshot pixels were shared");
|
|
});
|
|
|
|
it("keeps the original derivation marker when the review prompt is edited", async () => {
|
|
reportDraft.mockResolvedValueOnce({ kind: "draft-ready", report: { userPrompt: "It crashes", sourcePrompt: "It crashes", body: "## Summary\nIt crashes\n\n## Environment\nCollected context", context: {} } });
|
|
reportFile.mockResolvedValueOnce({ kind: "filed", url: "https://example.test/1" });
|
|
render(<ReportModal actionType="bug" onClose={vi.fn()} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("What went wrong?"), { target: { value: "It crashes" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
await screen.findByText("Review your report");
|
|
fireEvent.change(screen.getByLabelText("Report summary"), { target: { value: "It crashes after saving" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "File report" }));
|
|
|
|
await waitFor(() => expect(reportFile).toHaveBeenCalledOnce());
|
|
expect(reportFile).toHaveBeenCalledWith(expect.objectContaining({ report: expect.objectContaining({
|
|
userPrompt: "It crashes after saving",
|
|
sourcePrompt: "It crashes",
|
|
body: expect.stringContaining("## Environment"),
|
|
}) }));
|
|
});
|
|
});
|