Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions integrations/crowd-reports/ReportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import WarningIcon from "@mui/icons-material/Warning";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
Expand Down Expand Up @@ -134,6 +135,12 @@ export function ReportDialog() {
setSeverity(defaultSeverityForCategory(c));
};

// A failed submit's error belongs to this report, not the next one.
const handleClose = () => {
submit.reset();
closeDialog();
};

const handleSubmit = () => {
if (!location) return;
const claim = buildReportClaim({
Expand All @@ -154,7 +161,7 @@ export function ReportDialog() {
return (
<Dialog
open={open}
onClose={closeDialog}
onClose={handleClose}
fullWidth
maxWidth="sm"
fullScreen={fullScreen}
Expand Down Expand Up @@ -353,13 +360,25 @@ export function ReportDialog() {
</Button>
</Stack>
</Box>

{submit.isError && <Alert severity="error">{t("submitError")}</Alert>}
</Stack>
</DialogContent>
{/* Outside the scrolling content: on a phone the form overflows, and an
error at its end would sit below the fold, so a failed submit would
look like nothing happened. */}
{submit.isError && (
<Alert severity="error" sx={{ mx: 2, mt: 1.5 }}>
{t("submitError")}
</Alert>
)}
<DialogActions>
<Button onClick={closeDialog}>{t("cancel")}</Button>
<Button variant="contained" onClick={handleSubmit} disabled={!location || submit.isPending}>
<Button onClick={handleClose}>{t("cancel")}</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={!location || submit.isPending}
aria-busy={submit.isPending}
startIcon={submit.isPending ? <CircularProgress size={16} color="inherit" /> : undefined}
>
{t("submit")}
</Button>
</DialogActions>
Expand Down
75 changes: 75 additions & 0 deletions integrations/crowd-reports/__tests__/ReportDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// @vitest-environment jsdom

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen, userEvent } from "@/test";
import { ReportDialog } from "../ReportDialog";
import { useCrowdReportStore } from "../store";

vi.mock("next-intl", async () => (await import("@/test/intl")).mockNextIntl());
vi.mock("@/integration-api/components/LocationMinimap", () => ({
LocationMinimap: () => null,
}));
vi.mock("@/integration-api/runtime/useFullScreenOnMobile", () => ({
useFullScreenOnMobile: () => true,
mobileFullScreenDialogPaperSx: {},
}));
const { submitState } = vi.hoisted(() => ({
submitState: { mutate: vi.fn(), reset: vi.fn(), isError: false, isPending: false },
}));
vi.mock("../useCrowdReports", () => ({ useSubmitReport: () => submitState }));

describe("ReportDialog", () => {
beforeEach(() => {
useCrowdReportStore.getState().openDialog([13.74, 51.05]);
});

afterEach(() => {
useCrowdReportStore.getState().closeDialog();
Object.assign(submitState, { isError: false, isPending: false });
submitState.mutate.mockReset();
submitState.reset.mockReset();
});

it("submits the selected category at the chosen location", async () => {
render(<ReportDialog />);
await userEvent.click(screen.getByRole("button", { name: /crowdReports\.category\.jam/ }));
await userEvent.click(screen.getByRole("button", { name: "crowdReports.submit" }));

expect(submitState.mutate).toHaveBeenCalledWith(
expect.objectContaining({
kind: "congestion",
geometry: { type: "Point", coordinates: [13.74, 51.05] },
}),
expect.anything(),
);
});

it("shows a failed submit beside the actions, outside the scrolling form", () => {
submitState.isError = true;
render(<ReportDialog />);

const alert = screen.getByRole("alert");
expect(alert).toHaveTextContent("crowdReports.submitError");
// The form scrolls on a phone; an error at its end sits below the fold.
expect(alert.closest(".MuiDialogContent-root")).toBeNull();
});

it("clears a failed submit when the dialog is cancelled", async () => {
submitState.isError = true;
render(<ReportDialog />);
await userEvent.click(screen.getByRole("button", { name: "crowdReports.cancel" }));

expect(submitState.reset).toHaveBeenCalled();
expect(useCrowdReportStore.getState().open).toBe(false);
});

it("marks the submit button busy while the report is sent", () => {
submitState.isPending = true;
render(<ReportDialog />);

const submit = screen.getByRole("button", { name: "crowdReports.submit" });
expect(submit).toBeDisabled();
expect(submit).toHaveAttribute("aria-busy", "true");
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
});
Loading