// UpsellDialog — the locked screen in a modal (#369). No jsdom: react-dom/server // renders the markup in vitest's node environment, and calling the component as // a function (UpsellCtas.render.test.tsx pattern) exposes the props it hands // to UpsellGate and to its close button. import { describe, it, expect, vi } from "vitest"; import { isValidElement, type ReactElement, type ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { MemoryRouter } from "react-router-dom"; vi.mock("@tauri-apps/plugin-opener", () => ({ openUrl: vi.fn(() => Promise.resolve()) })); vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock()); // The locked screen reads one thing from the license context: a revoked key (#365). vi.mock("../../contexts/LicenseContext", () => ({ useLicenseContext: () => ({ revoked: false }) })); import UpsellDialog from "./UpsellDialog"; import UpsellGate from "./UpsellGate"; type Props = Record & { children?: ReactNode }; function find(node: ReactNode, pred: (el: ReactElement) => boolean): ReactElement[] { if (Array.isArray(node)) return node.flatMap((n) => find(n, pred)); if (!isValidElement(node)) return []; const el = node as ReactElement; return [...(pred(el) ? [el] : []), ...find(el.props.children, pred)]; } describe("UpsellDialog (#369)", () => { it("shows the locked screen of the feature in a named modal dialog", () => { const html = renderToStaticMarkup( , ); expect(html).toContain('role="dialog"'); expect(html).toContain('aria-modal="true"'); expect(html).toContain('aria-label="t(upsell.title[tier=t(license.editions.base)])"'); expect(html).toContain("t(upsell.features.adjustments)"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.base)])"); expect(html).toContain("t(upsell.ctaHaveKey)"); expect(html).toContain('aria-label="t(common.close)"'); }); it("closes on its X button and after 'I already have a key'", () => { const onClose = vi.fn(); const tree = UpsellDialog({ feature: "adjustments", onClose }); const gates = find(tree, (el) => el.type === UpsellGate); expect(gates).toHaveLength(1); expect(gates[0].props).toMatchObject({ feature: "adjustments", requiredTier: "base" }); expect(gates[0].props.onNavigate).toBe(onClose); const close = find(tree, (el) => el.type === "button" && el.props["aria-label"] === "t(common.close)"); expect(close).toHaveLength(1); (close[0].props.onClick as () => void)(); expect(onClose).toHaveBeenCalledTimes(1); }); it("asks for the tier the feature needs, not always Base", () => { const tree = UpsellDialog({ feature: "balance", onClose: vi.fn() }); expect(find(tree, (el) => el.type === UpsellGate)[0].props.requiredTier).toBe("premium"); }); });