diff --git a/frontend/src/components/AccountPanel.tsx b/frontend/src/components/AccountPanel.tsx index 42298122..7f91ac25 100644 --- a/frontend/src/components/AccountPanel.tsx +++ b/frontend/src/components/AccountPanel.tsx @@ -7,9 +7,9 @@ import { LoadingState, StateMessage } from "./ui/QueryState"; import { api } from "../lib/api"; import { qk } from "../lib/queryKeys"; import { notify } from "../lib/notifications"; -import { notifyActionError, notifyKnownFailure } from "../lib/actionErrors"; +import { notifyActionError } from "../lib/actionErrors"; import { copyText } from "../lib/clipboard"; -import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers"; +import { invalidateUserAccess } from "../lib/adminUsers"; import type { AdminUserAccess, AdminUserRow } from "../lib/api/admin"; // The admin's answer to "why can this person not get in?", and the place every recovery action for @@ -73,10 +73,7 @@ export default function AccountPanel({ const resend = useMutation({ mutationFn: () => api.resendUserVerification(user.id), onSuccess: () => notify({ level: "success", message: t("users.account.resendSent") }), - onError: (err) => - isEmailOff(err) - ? notifyKnownFailure(err, t("users.pendingAccess.resendEmailOff")) - : notifyActionError(err, t("users.pendingAccess.resendFailed")), + onError: (err) => notifyActionError(err, t("users.pendingAccess.resendFailed")), }); const activate = useMutation({ diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index 2bd5a7d2..916d5508 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -24,7 +24,7 @@ import { type PendingAccessRow, } from "../lib/api"; import { notify } from "../lib/notifications"; -import { invalidateUserAccess, isEmailOff } from "../lib/adminUsers"; +import { invalidateUserAccess } from "../lib/adminUsers"; import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab"; import { Button } from "./ui/Button"; import Tooltip from "./ui/Tooltip"; @@ -319,12 +319,6 @@ function UsersRoles({ me }: { me: Me }) { ); } -/** Did the invitation fail because this instance has no working SMTP? */ -function inviteEmailOff(err: HttpError): boolean { - const d = err.detailData as { code?: string; reason?: string } | null | undefined; - return d?.code === "invite_send" && d.reason === "email_off"; -} - // --- Sign-ups that are still outside (roadmap UX-25) -------------------------------------------- // // The gap this closes: approve an address BEFORE the person registers and their sign-up produces no @@ -360,12 +354,9 @@ function PendingAccess() { refresh(); notify({ level: "success", message: t("users.pendingAccess.resent", { email: u.email }) }); }, - // The public resend is deliberately silent; this one is not, so say WHY it failed — an instance - // with no working SMTP would otherwise show a success toast for a mail that was never sent. - onError: (err) => - isEmailOff(err) - ? notifyKnownFailure(err, t("users.pendingAccess.resendEmailOff")) - : notifyActionError(err, t("users.pendingAccess.resendFailed")), + // The public resend is deliberately silent; this one is not, and the server's reason says WHY + // (no working SMTP, a refused send) — a success toast for a mail never sent would be the lie. + onError: (err) => notifyActionError(err, t("users.pendingAccess.resendFailed")), }); const list = rows.data ?? []; @@ -591,10 +582,7 @@ function AdminInvites() { refresh(); notify({ level: "success", message: t("settings.invites.invited", { email: v.inv.email }) }); }, - onError: (err) => - err instanceof HttpError && inviteEmailOff(err) - ? notifyKnownFailure(err, t("settings.invites.inviteEmailOff")) - : notifyActionError(err, t("settings.invites.inviteFailed")), + onError: (err) => notifyActionError(err, t("settings.invites.inviteFailed")), }); // The one recipient the app has no language signal about: the admin typed this address in, so // nobody ever told us how to write to its owner. The dialog is where that gets decided — and it diff --git a/frontend/src/components/ChannelPage.busyFocus.dom.test.tsx b/frontend/src/components/ChannelPage.busyFocus.dom.test.tsx new file mode 100644 index 00000000..e676af89 --- /dev/null +++ b/frontend/src/components/ChannelPage.busyFocus.dom.test.tsx @@ -0,0 +1,93 @@ +// @vitest-environment jsdom +// +// The channel page's Block and Hide toggles while their request runs (deep-review E11 round 2). A +// focused button that turns natively `disabled` drops the keyboard to
in Chrome (measured for +// ui/Button in E11 S2), so a busy toggle blocks the click with aria-disabled instead — focusable, and +// still pressed only once. +import type { ReactNode } from "react"; +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, fireEvent, waitFor, cleanup, act } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +vi.mock("react-i18next", async (importOriginal) => ({ + ...(await importOriginal