Files
ApiCore/ApiCore-Frontend/src/components/auth/step-up-dialog.spec.tsx
T
Nick SzaunigandClaude Opus 5.5 7a1480138d fix: T4 browser-check findings, interval on season state, T6 plan refresh
- Honey batch create omits an empty batch number (was 400); product form
  labels the legacy total weight, detail shows jar weight and shelf life;
  bottling hints next to their fields, values cleared on product change,
  post-save water-content wording; withdraw reason placeholder not
  selectable; honey batch actions offline-disabled with reason, 44 px.
- Offline signal: one slow request no longer flips the app offline; two
  consecutive timeouts do, any HTTP answer clears it, aborts never count.
- Phenology sheet as a side card on desktop with focus return; confirm
  dialogs as alertdialog; step-up texts in du-form ("Das Passwort stimmt
  nicht."); backup import gives its disabled reason; missing event labels
  plus a guard test; profile buttons 44 px.
- GET /phenology/state returns intervalDays via the shared
  inspectionIntervalOf (autopilot, digest, season state use one source).
- T6 plan aligned with the code after T4, plus owner decisions of
  2026-10-15 (Berlin day for restrictions, COMPLIANCE category, override
  without step-up).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 12:47:08 +02:00

120 lines
4.8 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { StepUpDialog } from "./step-up-dialog";
import { ApiError } from "@/lib/auth/api-client";
function renderDialog(overrides: Partial<Parameters<typeof StepUpDialog>[0]> = {}) {
const onConfirm = vi.fn().mockResolvedValue(undefined);
const onOpenChange = vi.fn();
render(
<StepUpDialog
open
title="Telegram verbinden?"
confirmLabel="Verbinden"
needsMfa={false}
onConfirm={onConfirm}
onOpenChange={onOpenChange}
{...overrides}
/>,
);
return { onConfirm, onOpenChange };
}
describe("StepUpDialog", () => {
it("asks for the password only without MFA and confirms with it", async () => {
const { onConfirm, onOpenChange } = renderDialog();
expect(screen.queryByLabelText("Code aus der Authenticator-App")).not.toBeInTheDocument();
const confirm = screen.getByRole("button", { name: "Verbinden" });
expect(confirm).toBeDisabled();
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(confirm);
await waitFor(() => expect(onConfirm).toHaveBeenCalledWith({ password: "geheim" }));
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
});
it("asks for the six-digit code with MFA and sends both", async () => {
const { onConfirm } = renderDialog({ needsMfa: true });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
const confirm = screen.getByRole("button", { name: "Verbinden" });
await userEvent.type(screen.getByLabelText("Code aus der Authenticator-App"), "12ab34");
expect(confirm).toBeDisabled();
await userEvent.type(screen.getByLabelText("Code aus der Authenticator-App"), "56");
expect(confirm).toBeEnabled();
await userEvent.click(confirm);
await waitFor(() =>
expect(onConfirm).toHaveBeenCalledWith({ password: "geheim", mfaCode: "123456" }),
);
});
it("stays open and shows the server's German reason when the proof is refused", async () => {
const onConfirm = vi.fn().mockRejectedValue(new ApiError(401, "Das Passwort ist falsch."));
const { onOpenChange } = renderDialog({ onConfirm });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "falsch");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
expect(screen.getByRole("alert")).toHaveTextContent("Das Passwort ist falsch."),
);
expect(onOpenChange).not.toHaveBeenCalledWith(false);
expect(screen.getByLabelText("Dein Passwort")).toHaveValue("");
expect(screen.getByLabelText("Dein Passwort")).toHaveFocus();
});
it("shows the German too-many-attempts text on 429", async () => {
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(429, "ThrottlerException")) });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "x");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
expect(screen.getByRole("alert")).toHaveTextContent(
"Zu viele Versuche. Bitte in einer Minute noch einmal.",
),
);
});
it("says the session expired on a bare 401", async () => {
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(401, "Unauthorized")) });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "x");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
expect(screen.getByRole("alert")).toHaveTextContent(
"Sitzung abgelaufen. Bitte die Seite neu laden und erneut anmelden.",
),
);
});
it("makes the inputs read-only while the action runs", async () => {
renderDialog({ onConfirm: vi.fn(() => new Promise<void>(() => {})) });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() => expect(screen.getByLabelText("Dein Passwort")).toHaveAttribute("readonly"));
});
it("cannot be closed with Escape while the action runs, and shows the pending label", async () => {
const onConfirm = vi.fn(() => new Promise<void>(() => {}));
const { onOpenChange } = renderDialog({ onConfirm, pendingLabel: "Wird eingespielt…" });
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
expect(await screen.findByRole("button", { name: "Wird eingespielt…" })).toBeDisabled();
await userEvent.keyboard("{Escape}");
await userEvent.click(screen.getByRole("button", { name: /Close/ }));
expect(onOpenChange).not.toHaveBeenCalled();
expect(screen.getByRole("dialog")).toBeInTheDocument();
});
});