diff --git a/.cursor/rules/alerts.mdc b/.cursor/rules/alerts.mdc new file mode 100644 index 0000000..dfd2dc5 --- /dev/null +++ b/.cursor/rules/alerts.mdc @@ -0,0 +1,48 @@ +--- +description: Unified Alert (toast/banner) for app notifications — Figma + drift prevention +globs: app/**/*.tsx, stories/modals/Alert.stories.js, tests/components/Alert.test.tsx +alwaysApply: false +--- + +# Alerts and notifications + +## Source of truth + +- **Figma:** [Community Rule System — Modal / Alert](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=6351-14646) (node **6351-14646**). +- **Code:** `app/components/modals/Alert` — default export `Alert` from `Alert.container.tsx` (Figma docstring on the container). + +## When to use `Alert` + +Use **`Alert`** for **app-level, section-level, and shell-level** success, warning, error, and neutral status messages that should read as a designed system surface (not body copy alone). + +Do **not** recreate the same job with ad-hoc UI: bordered `

`, free-standing `role="alert"` blocks, or raw `text-[var(--color-border-default-utility-negative)]` paragraphs for product messaging. + +## Props (lowercase in code; match Figma intent) + +| Concern | Prop | Notes | +| --- | --- | --- | +| Layout | `type` | `toast` — bottom accent bar, top rounded corners; `banner` — full rounded block, inline or stacked. | +| Intent | `status` | `default` \| `positive` \| `warning` \| `danger`. | +| Density | `size` | `s` \| `m` (Figma S/M). Typography and padding are implemented inside `Alert.container.tsx` — do not fork spacing per call site. | +| Copy | `title`, `description?` | Required title; optional description when `hasBodyText` is true. | +| Icon | `hasLeadingIcon?` | Default `true`. | +| Body | `hasBodyText?` | Default `true`; set `false` for title-only. | +| Dismiss | `onClose?`, `hasTrailingIcon?` | Close control shows only when `onClose` is provided **and** `hasTrailingIcon` is not `false`. Omit `onClose` for non-dismissible messages. | + +Valid enum slices for Storybook / guards: `ALERT_*_OPTIONS` in `lib/propNormalization.ts`. + +## Choosing toast vs banner + +- **`toast`** — transient edge / bottom emphasis (e.g. completed flow), strong bottom border accent. +- **`banner`** — rounded block; for **page / shell / modal** messaging, mount inside a **`fixed`** (or equivalent) overlay wrapper with `pointer-events-none` on the outer layer and `pointer-events-auto` on the alert so layout chrome does not reflow when the message appears (see `CreateFlowLayoutClient` `topBanners`, profile overlays, `LoginForm`, `PostLoginDraftTransfer`). + +## Exemptions (do not force `Alert`) + +1. **Single-field validation** under a control — keep `TextInput` / `TextArea` `error` and helper text (e.g. invalid email on the login form) unless design explicitly moves that line into `Alert`. +2. **Marketing layout** — `HeroBanner`, `ContentBanner` are not system alerts. +3. **Landmarks** — `role="banner"` on headers/nav is not the `Alert` “banner” type. +4. **A11y-only live regions** — e.g. tooltip / incrementer `aria-live` for widget state, not product notifications. + +## Copy + +All user-visible strings go through **`messages/`** and `useTranslation` / message modules per `localization.mdc`. diff --git a/AGENTS.md b/AGENTS.md index c750780..5c26812 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Single-locale (English) today; designed for i18n via `messages/`. | If you're touching… | Load this rule | | --- | --- | | `app/components/**` | `component-structure.mdc`, `component-props.mdc`, `tailwind-styling.mdc` | +| `Alert`, or user-visible notifications / shell errors / success banners | `alerts.mdc` (and `localization.mdc` for copy) | | `app/(app)/create/**` | `create-flow.mdc` (+ component rules) | | `app/api/**` | `api-routes.mdc` | | `app/hooks/**` | `hooks.mdc` | diff --git a/app/(app)/create/PostLoginDraftTransfer.tsx b/app/(app)/create/PostLoginDraftTransfer.tsx index bf56460..178469e 100644 --- a/app/(app)/create/PostLoginDraftTransfer.tsx +++ b/app/(app)/create/PostLoginDraftTransfer.tsx @@ -11,6 +11,7 @@ import { useCreateFlow } from "./context/CreateFlowContext"; import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps"; import { saveDraftToServer } from "../../../lib/create/api"; import messages from "../../../messages/en/index"; +import Alert from "../../components/modals/Alert"; const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true"; @@ -139,12 +140,27 @@ export function PostLoginDraftTransfer({ if (!transferError) return null; + const [titleLine, ...rest] = transferError.split(/\n\n+/); + const title = (titleLine ?? transferError).trim(); + const description = rest.join("\n\n").trim() || undefined; + return ( -

- {transferError} +
+
+ { + setTransferError(null); + }} + className="w-full" + /> +
); } diff --git a/app/(app)/create/SignedInDraftHydration.tsx b/app/(app)/create/SignedInDraftHydration.tsx index c7e6c74..b53364f 100644 --- a/app/(app)/create/SignedInDraftHydration.tsx +++ b/app/(app)/create/SignedInDraftHydration.tsx @@ -11,6 +11,7 @@ import { import { useCreateFlow } from "./context/CreateFlowContext"; import { fetchDraftFromServer } from "../../../lib/create/api"; import messages from "../../../messages/en/index"; +import Alert from "../../components/modals/Alert"; import { isValidStep, parseCreateFlowScreenFromPathname, @@ -119,12 +120,19 @@ export function SignedInDraftHydration({ if (!loadingHydration) return null; return ( -
- {messages.create.draftHydration.loadingSavedProgress} +
+
+ +
); } diff --git a/app/(app)/create/review-template/[slug]/page.tsx b/app/(app)/create/review-template/[slug]/page.tsx index ba38dff..f0106eb 100644 --- a/app/(app)/create/review-template/[slug]/page.tsx +++ b/app/(app)/create/review-template/[slug]/page.tsx @@ -86,19 +86,28 @@ export default function ReviewTemplatePage({ params }: PageProps) { if (error || !template) { return ( - + <>
- +
+ +
-
+ +
+ + ); } diff --git a/app/(app)/profile/ProfilePageClient.tsx b/app/(app)/profile/ProfilePageClient.tsx index 97c068e..aeb6446 100644 --- a/app/(app)/profile/ProfilePageClient.tsx +++ b/app/(app)/profile/ProfilePageClient.tsx @@ -62,6 +62,7 @@ export default function ProfilePageClient() { const [emailChangeModalError, setEmailChangeModalError] = useState< string | null >(null); + const [emailChangeRequestSent, setEmailChangeRequestSent] = useState(false); const [profileSuccessMessage, setProfileSuccessMessage] = useState< string | null >(null); @@ -131,6 +132,7 @@ export default function ProfilePageClient() { setActionError(null); setProfileSuccessMessage(null); setEmailChangeModalError(null); + setEmailChangeRequestSent(false); setEmailChangeInput(user.email); setEmailChangeOpen(true); }, [user]); @@ -138,8 +140,25 @@ export default function ProfilePageClient() { const handleCloseEmailChange = useCallback(() => { if (emailChangeBusy) return; setEmailChangeOpen(false); + setEmailChangeRequestSent(false); }, [emailChangeBusy]); + const handleDismissProfileSuccess = useCallback(() => { + setProfileSuccessMessage(null); + }, []); + + const handleDismissActionError = useCallback(() => { + setActionError(null); + }, []); + + const handleDismissRulesError = useCallback(() => { + setRulesError(false); + }, []); + + const handleDismissEmailChangeModalError = useCallback(() => { + setEmailChangeModalError(null); + }, []); + const handleSubmitEmailChange = useCallback(async () => { const trimmed = emailChangeInput.trim(); if (!trimmed || emailChangeBusy) return; @@ -157,8 +176,7 @@ export default function ProfilePageClient() { setEmailChangeModalError(res.error); } } else { - setEmailChangeOpen(false); - setProfileSuccessMessage(t("emailChangeRequestSent")); + setEmailChangeRequestSent(true); } }, [emailChangeBusy, emailChangeInput, t]); @@ -318,7 +336,12 @@ export default function ProfilePageClient() { emailChangeValue={emailChangeInput} onEmailChangeValueChange={(value) => setEmailChangeInput(value)} emailChangeBusy={emailChangeBusy} + emailChangeRequestSent={emailChangeRequestSent} emailChangeModalError={emailChangeModalError} + onDismissProfileSuccess={handleDismissProfileSuccess} + onDismissActionError={handleDismissActionError} + onDismissRulesError={handleDismissRulesError} + onDismissEmailChangeModalError={handleDismissEmailChangeModalError} onOpenEmailChange={handleOpenEmailChange} onCloseEmailChange={handleCloseEmailChange} onSubmitEmailChange={handleSubmitEmailChange} diff --git a/app/(app)/profile/_components/ProfilePage.view.tsx b/app/(app)/profile/_components/ProfilePage.view.tsx index 9f6790a..e93877a 100644 --- a/app/(app)/profile/_components/ProfilePage.view.tsx +++ b/app/(app)/profile/_components/ProfilePage.view.tsx @@ -6,7 +6,9 @@ import RuleCard from "../../../components/cards/RuleCard"; import TextInput from "../../../components/controls/TextInput"; import List from "../../../components/layout/List"; import type { ListItem, ListSize } from "../../../components/layout/List"; +import Icon from "../../../components/asset/Icon"; import Dialog from "../../../components/modals/Dialog"; +import Alert from "../../../components/modals/Alert"; import HeaderLockup from "../../../components/type/HeaderLockup"; import { useTranslation } from "../../../contexts/MessagesContext"; import type { CreateFlowState } from "../../create/types"; @@ -49,7 +51,9 @@ export type ProfilePageViewProps = { emailChangeValue: string; onEmailChangeValueChange: (value: string) => void; emailChangeBusy: boolean; + emailChangeRequestSent: boolean; emailChangeModalError: string | null; + onDismissEmailChangeModalError: () => void; onOpenEmailChange: () => void; onCloseEmailChange: () => void; onSubmitEmailChange: () => void; @@ -65,6 +69,9 @@ export type ProfilePageViewProps = { onOpenDeleteAccount: () => void; onCloseDeleteAccount: () => void; onConfirmDeleteAccount: () => void; + onDismissProfileSuccess: () => void; + onDismissActionError: () => void; + onDismissRulesError: () => void; }; /** @@ -171,7 +178,9 @@ export function ProfilePageView({ emailChangeValue, onEmailChangeValueChange, emailChangeBusy, + emailChangeRequestSent, emailChangeModalError, + onDismissEmailChangeModalError, onOpenEmailChange, onCloseEmailChange, onSubmitEmailChange, @@ -187,8 +196,12 @@ export function ProfilePageView({ onOpenDeleteAccount, onCloseDeleteAccount, onConfirmDeleteAccount, + onDismissProfileSuccess, + onDismissActionError, + onDismissRulesError, }: ProfilePageViewProps) { const t = useTranslation("pages.profile"); + const tLogin = useTranslation("pages.login"); const titleId = useId(); const welcomeTitle = t("welcomeTitle").replace(/\{\{name\}\}/g, userEmail); const welcomeBody = @@ -277,30 +290,6 @@ export function ProfilePageView({ )} - {profileSuccessMessage ? ( -

- {profileSuccessMessage} -

- ) : null} - - {actionError ? ( -

- {actionError} -

- ) : null} - - {rulesError ? ( -

- {t("actionError")} -

- ) : null} -
+ emailChangeRequestSent ? ( - - + ) : ( + <> + + + + ) } > - {emailChangeModalError ? ( -

- {emailChangeModalError} -

- ) : null} - onEmailChangeValueChange(e.target.value)} - disabled={emailChangeBusy} - error={Boolean(emailChangeModalError)} - autoComplete="email" - /> + {emailChangeRequestSent ? ( +
+
+ +
+
+ ) : ( + onEmailChangeValueChange(e.target.value)} + disabled={emailChangeBusy} + error={Boolean(emailChangeModalError)} + autoComplete="email" + /> + )} + + {(profileSuccessMessage || actionError || rulesError) && ( +
+
+ {profileSuccessMessage ? ( + + ) : null} + {actionError ? ( + + ) : null} + {rulesError ? ( + + ) : null} +
+
+ )} + + {emailChangeOpen && emailChangeModalError ? ( +
+
+ +
+
+ ) : null} ); } diff --git a/app/components/modals/Alert/Alert.container.tsx b/app/components/modals/Alert/Alert.container.tsx index cdd678d..ce01a55 100644 --- a/app/components/modals/Alert/Alert.container.tsx +++ b/app/components/modals/Alert/Alert.container.tsx @@ -1,52 +1,117 @@ "use client"; +/** + * Figma: "Modal / Alert" (6351-14646) + * https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=6351-14646 + */ + import { memo } from "react"; import { AlertView } from "./Alert.view"; import type { AlertProps } from "./Alert.types"; +function layoutFor( + type: NonNullable, + size: NonNullable, +): { + containerClasses: string; + titleClasses: string; + descriptionClasses: string; +} { + if (type === "toast") { + const padH = + size === "s" + ? "px-[var(--space-500)]" + : "px-[var(--space-1200)]"; + const containerClasses = `flex gap-[var(--space-300)] items-center ${padH} pb-[var(--space-500)] pt-[var(--space-400)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)] border-solid`; + if (size === "s") { + return { + containerClasses, + titleClasses: + "font-inter text-[14px] leading-[18px] font-medium tracking-[0%]", + descriptionClasses: + "font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]", + }; + } + return { + containerClasses, + titleClasses: + "font-inter text-[18px] leading-[24px] font-medium tracking-[0%]", + descriptionClasses: + "font-inter text-[18px] leading-[1.3] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]", + }; + } + + if (size === "s") { + return { + containerClasses: + "flex gap-[var(--space-300)] items-center p-[var(--space-300)] rounded-[var(--radius-200,8px)] border-solid", + titleClasses: + "font-inter text-[14px] leading-[18px] font-medium tracking-[0%]", + descriptionClasses: + "font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]", + }; + } + return { + containerClasses: + "flex gap-[var(--space-300)] items-center px-[var(--space-600)] py-[var(--space-400)] rounded-[var(--radius-200,8px)] border-solid", + titleClasses: + "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]", + descriptionClasses: + "font-inter text-[16px] leading-[24px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]", + }; +} + const AlertContainer = memo( ({ title, description, status: statusProp = "default", type: typeProp = "toast", + size: sizeProp = "m", hasLeadingIcon = true, hasBodyText = true, + hasTrailingIcon: hasTrailingIconProp, onClose, className = "", }) => { const status = statusProp; const type = typeProp; - // Determine background and border colors based on status and type + const size = sizeProp; + const getStatusStyles = () => { switch (status) { case "positive": return { - background: "bg-[var(--color-kiwi-kiwi0)]", + background: + "bg-[var(--color-surface-invert-positive-secondary,var(--color-kiwi-kiwi0))]", borderColor: type === "toast" ? "var(--color-border-invert-positive-primary)" : undefined, titleColor: "text-[var(--color-content-invert-primary)]", - descriptionColor: "text-[var(--color-content-invert-secondary)]", + descriptionColor: + "text-[var(--color-content-invert-secondary)]", iconColor: "var(--color-kiwi-kiwi500)", closeButtonIconColor: "var(--color-content-invert-primary)", }; case "warning": return { - background: "bg-[var(--color-yellow-yellow0)]", + background: + "bg-[var(--color-surface-invert-warning-secondary,var(--color-yellow-yellow0))]", borderColor: type === "toast" ? "var(--color-border-invert-warning-primary)" : undefined, titleColor: "text-[var(--color-content-invert-primary)]", - descriptionColor: "text-[var(--color-content-invert-secondary)]", + descriptionColor: + "text-[var(--color-content-invert-secondary)]", iconColor: "var(--color-yellow-yellow500)", closeButtonIconColor: "var(--color-content-invert-primary)", }; case "danger": return { - background: "bg-[var(--color-red-red0)]", + background: + "bg-[var(--color-surface-invert-negative-secondary,var(--color-red-red0))]", borderColor: type === "toast" ? "var(--color-border-invert-negative-primary)" @@ -67,18 +132,14 @@ const AlertContainer = memo( titleColor: "text-[var(--color-content-default-primary)]", descriptionColor: "text-[var(--color-content-default-primary)]", iconColor: "var(--color-content-default-brand-primary)", - closeButtonIconColor: "var(--color-content-default-brand-primary)", + closeButtonIconColor: + "var(--color-content-default-brand-primary)", }; } }; const statusStyles = getStatusStyles(); - - const containerClasses = `flex gap-[var(--space-300)] items-center ${ - type === "toast" - ? `pb-[var(--space-500)] pt-[var(--space-400)] px-[var(--space-1200)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)]` - : `px-[var(--spacing-scale-024)] py-[var(--spacing-scale-016)] rounded-[var(--radius-200,8px)]` - } ${statusStyles.background} border-solid`; + const layout = layoutFor(type, size); const containerStyle = type === "toast" && statusStyles.borderColor @@ -88,15 +149,14 @@ const AlertContainer = memo( } : undefined; - const titleClasses = - type === "banner" - ? `font-inter text-[16px] leading-[20px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full` - : `font-inter text-[18px] leading-[24px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`; + const containerClasses = `${layout.containerClasses} ${statusStyles.background}`; - const descriptionClasses = - type === "banner" - ? `font-inter text-[16px] leading-[24px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]` - : `font-inter text-[18px] leading-[23.4px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`; + const titleClasses = `${layout.titleClasses} ${statusStyles.titleColor} relative shrink-0 w-full`; + const descriptionClasses = `${layout.descriptionClasses} ${statusStyles.descriptionColor} relative shrink-0 w-full`; + + const hasTrailingIcon = + hasTrailingIconProp ?? Boolean(onClose); + const showClose = hasTrailingIcon && Boolean(onClose); return ( ( type={type} hasLeadingIcon={hasLeadingIcon} hasBodyText={hasBodyText} + hasTrailingIcon={showClose} className={className} containerClasses={containerClasses} containerStyle={containerStyle} diff --git a/app/components/modals/Alert/Alert.types.ts b/app/components/modals/Alert/Alert.types.ts index e01a8d2..3278b1b 100644 --- a/app/components/modals/Alert/Alert.types.ts +++ b/app/components/modals/Alert/Alert.types.ts @@ -1,7 +1,11 @@ +import type { AlertSizeValue } from "../../../../lib/propNormalization"; + export type AlertStatusValue = "default" | "positive" | "warning" | "danger"; export type AlertTypeValue = "toast" | "banner"; +export type { AlertSizeValue }; + export interface AlertProps { title: string; description?: string; @@ -13,6 +17,11 @@ export interface AlertProps { * Alert type. */ type?: AlertTypeValue; + /** + * Density / typography scale (Figma Modal Alert S | M). + * @default "m" + */ + size?: AlertSizeValue; /** * Whether to show the leading icon (Figma prop). * @default true @@ -23,6 +32,11 @@ export interface AlertProps { * @default true */ hasBodyText?: boolean; + /** + * Trailing dismiss control (Figma `hasTrailingIcon`). + * When omitted, defaults to `true` when `onClose` is provided, else `false`. + */ + hasTrailingIcon?: boolean; onClose?: () => void; className?: string; } @@ -34,6 +48,7 @@ export interface AlertViewProps { type: "toast" | "banner"; hasLeadingIcon: boolean; hasBodyText: boolean; + hasTrailingIcon: boolean; className: string; containerClasses: string; containerStyle?: React.CSSProperties; diff --git a/app/components/modals/Alert/Alert.view.tsx b/app/components/modals/Alert/Alert.view.tsx index 4482230..08a67b6 100644 --- a/app/components/modals/Alert/Alert.view.tsx +++ b/app/components/modals/Alert/Alert.view.tsx @@ -8,6 +8,7 @@ export function AlertView({ type: _type, hasLeadingIcon, hasBodyText, + hasTrailingIcon, className, containerClasses, containerStyle, @@ -54,40 +55,42 @@ export function AlertView({

{description}

)}
- + + + + + + + + + ) : null}
); } diff --git a/app/components/modals/Login/LoginForm.tsx b/app/components/modals/Login/LoginForm.tsx index 2276c84..02abeb4 100644 --- a/app/components/modals/Login/LoginForm.tsx +++ b/app/components/modals/Login/LoginForm.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "../../../contexts/MessagesContext"; import Button from "../../buttons/Button"; import TextInput from "../../controls/TextInput"; import ContentLockup from "../../type/ContentLockup"; +import Alert from "../Alert"; import { requestMagicLink } from "../../../../lib/create/api"; import { safeInternalPath } from "../../../../lib/safeInternalPath"; import { setTransferPendingFlag } from "../../../(app)/create/utils/anonymousDraftStorage"; @@ -55,7 +56,6 @@ export default function LoginForm({ const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); - const formAlertId = useId(); const emailErrorId = useId(); const [email, setEmail] = useState(""); @@ -166,26 +166,40 @@ export default function LoginForm({ />
- {urlErrorMessage ? ( -

- {urlErrorMessage} -

- ) : null} - - {formError ? ( - - ) : null} + {(urlErrorMessage || formError) && ( +
+
+ {urlErrorMessage ? ( + { + stripErrorQuery(); + }} + className="w-full" + /> + ) : null} + {formError ? ( + { + setFormError(""); + }} + className="w-full" + /> + ) : null} +
+
+ )} {!sent ? (
( +
+ +
+); +export const BannerNoDismiss = NoDismissTemplate.bind({}); +BannerNoDismiss.args = { + title: "Non-dismissible banner (no onClose)", + description: "Used when the message clears via navigation or parent state only.", + status: "danger", + type: "banner", + size: "s", +}; + export const AllStatuses = () => { const [visible, setVisible] = useState({ default: true, diff --git a/tests/components/Alert.test.tsx b/tests/components/Alert.test.tsx index b0b4826..526de4d 100644 --- a/tests/components/Alert.test.tsx +++ b/tests/components/Alert.test.tsx @@ -1,6 +1,10 @@ import React from "react"; +import { describe, expect, it, vi } from "vitest"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import Alert from "../../app/components/modals/Alert"; import { componentTestSuite } from "../utils/componentTestSuite"; +import { renderWithProviders as render } from "../utils/test-utils"; type AlertProps = React.ComponentProps; @@ -16,6 +20,7 @@ componentTestSuite({ description: "Optional description", status: "positive", type: "banner", + size: "m", }, primaryRole: "alert", testCases: { @@ -26,3 +31,30 @@ componentTestSuite({ errorState: false, }, }); + +describe("Alert dismiss control", () => { + it("omits close button when onClose is absent", () => { + render(); + expect( + screen.queryByRole("button", { name: "Close alert" }), + ).not.toBeInTheDocument(); + }); + + it("renders close when onClose is provided", async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + render(); + await user.click(screen.getByRole("button", { name: "Close alert" })); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("omits close when hasTrailingIcon is false even if onClose exists", () => { + const onClose = vi.fn(); + render( + , + ); + expect( + screen.queryByRole("button", { name: "Close alert" }), + ).not.toBeInTheDocument(); + }); +});