import { Toasty } from "@cloudflare/kumo"; import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { userEvent } from "vitest/browser"; import type { MediaItem, SiteSettings } from "../../../src/lib/api"; import { render } from "../../utils/render"; const mockFetchSettings = vi.fn<() => Promise>>(); const mockUpdateSettings = vi.fn<(settings: Partial) => Promise>>(); vi.mock("@tanstack/react-router", async () => { const actual = await vi.importActual("@tanstack/react-router"); return { ...actual, Link: ({ children, to, ...props }: any) => ( {children} ), }; }); vi.mock("../../../src/lib/api", async () => { const actual = await vi.importActual("../../../src/lib/api"); return { ...actual, fetchSettings: () => mockFetchSettings(), updateSettings: (settings: Partial) => mockUpdateSettings(settings), }; }); vi.mock("../../../src/components/MediaPickerModal", () => ({ MediaPickerModal: ({ open, title, onSelect, }: { open: boolean; title: React.ReactNode; onSelect: (media: MediaItem) => void; }) => { if (!open) return null; const modalTitle = typeof title === "string" ? title : ""; return (
); }, })); const { SeoSettings } = await import("../../../src/components/settings/SeoSettings"); const defaultSettings: Partial = { title: "My Blog", seo: { titleSeparator: "—", defaultOgImage: { mediaId: "existing-og-image", alt: "Existing social card", url: "/media/existing-social-card.png", }, googleVerification: "google-code", bingVerification: "bing-code", robotsTxt: "User-agent: *\nDisallow: /private/", }, }; function Wrapper({ children }: { children: React.ReactNode }) { return {children}; } async function renderSeoSettings() { return render(, { wrapper: Wrapper }); } beforeEach(() => { vi.clearAllMocks(); mockFetchSettings.mockResolvedValue(defaultSettings); mockUpdateSettings.mockImplementation(async (settings) => { mockFetchSettings.mockResolvedValue(settings); return settings; }); }); describe("SeoSettings", () => { it("shows the shared frame while settings load", async () => { mockFetchSettings.mockReturnValue(new Promise(() => undefined)); const screen = await renderSeoSettings(); await expect .element(screen.getByRole("heading", { name: "SEO Settings", level: 1 })) .toBeInTheDocument(); await expect.element(screen.getByText("Loading settings...")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Save" }).query()).toBeNull(); }); it("shows a load failure without form actions", async () => { mockFetchSettings.mockRejectedValue(new Error("Settings service unavailable")); const screen = await renderSeoSettings(); await expect.element(screen.getByRole("alert")).toHaveTextContent("An error occurred"); await expect .element(screen.getByRole("alert")) .toHaveTextContent("Settings service unavailable"); expect(screen.getByRole("button", { name: "Save" }).query()).toBeNull(); }); it("renders initial SEO values with both save actions pristine", async () => { const screen = await renderSeoSettings(); await expect.element(screen.getByLabelText("Title Separator")).toHaveValue("—"); await expect .element(screen.getByRole("textbox", { name: "Google Verification", exact: true })) .toHaveValue("google-code"); await expect .element(screen.getByRole("textbox", { name: "Bing Verification", exact: true })) .toHaveValue("bing-code"); await expect .element(screen.getByRole("textbox", { name: "robots.txt", exact: true })) .toHaveValue("User-agent: *\nDisallow: /private/"); await expect .element(screen.getByRole("img", { name: "Existing social card" })) .toBeInTheDocument(); const saveButtons = screen.getByRole("button", { name: "Saved", exact: true }).all(); expect(saveButtons).toHaveLength(2); for (const button of saveButtons) await expect.element(button).toBeDisabled(); }); it("submits the bottom save action and returns both actions to pristine", async () => { const screen = await renderSeoSettings(); await screen .getByRole("textbox", { name: "Google Verification", exact: true }) .fill("updated-google-code"); const dirtyButtons = screen.getByRole("button", { name: "Save", exact: true }).all(); expect(dirtyButtons).toHaveLength(2); for (const button of dirtyButtons) await expect.element(button).toBeEnabled(); await userEvent.click(dirtyButtons[1]); await vi.waitFor(() => { expect(mockUpdateSettings).toHaveBeenCalledWith({ ...defaultSettings, seo: { ...defaultSettings.seo, googleVerification: "updated-google-code", }, }); }); await expect.element(screen.getByText("SEO settings saved")).toBeInTheDocument(); const savedButtons = screen.getByRole("button", { name: "Saved", exact: true }).all(); expect(savedButtons).toHaveLength(2); for (const button of savedButtons) await expect.element(button).toBeDisabled(); }); it("keeps cached SEO settings visible when the post-save refetch fails", async () => { mockUpdateSettings.mockImplementation(async (settings) => { mockFetchSettings.mockRejectedValue(new Error("Settings refetch failed")); return settings; }); const screen = await renderSeoSettings(); await screen .getByRole("textbox", { name: "Google Verification", exact: true }) .fill("updated-google-code"); await userEvent.click(screen.getByRole("button", { name: "Save", exact: true }).all()[0]); await vi.waitFor(() => expect(mockFetchSettings.mock.calls.length).toBeGreaterThanOrEqual(2)); await expect .element(screen.getByRole("textbox", { name: "Google Verification", exact: true })) .toHaveValue("updated-google-code"); expect(screen.getByRole("alert").query()).toBeNull(); }); it("keeps the form dirty when the header save action fails", async () => { mockUpdateSettings.mockRejectedValue(new Error("Could not persist settings")); const screen = await renderSeoSettings(); await screen .getByRole("textbox", { name: "Bing Verification", exact: true }) .fill("updated-bing-code"); await userEvent.click(screen.getByRole("button", { name: "Save", exact: true }).all()[0]); await expect.element(screen.getByText("Failed to save settings")).toBeInTheDocument(); await expect.element(screen.getByText("Could not persist settings")).toBeInTheDocument(); const dirtyButtons = screen.getByRole("button", { name: "Save", exact: true }).all(); expect(dirtyButtons).toHaveLength(2); for (const button of dirtyButtons) await expect.element(button).toBeEnabled(); }); it("selects and removes a default social image", async () => { mockFetchSettings.mockResolvedValue({ ...defaultSettings, seo: { ...defaultSettings.seo, defaultOgImage: undefined }, }); const screen = await renderSeoSettings(); await userEvent.click(screen.getByRole("button", { name: "Select Image" })); await expect .element(screen.getByRole("dialog", { name: "Select Default Social Image" })) .toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Choose image" })); await expect.element(screen.getByRole("img", { name: "Social card" })).toBeInTheDocument(); for (const button of screen.getByRole("button", { name: "Save", exact: true }).all()) { await expect.element(button).toBeEnabled(); } await userEvent.click(screen.getByRole("button", { name: "Save", exact: true }).all()[0]); await vi.waitFor(() => { expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ seo: expect.objectContaining({ defaultOgImage: { mediaId: "new-og-image", alt: "Social card", url: "/media/social-card.png", }, }), }), ); }); await userEvent.click(screen.getByRole("button", { name: "Remove" })); await expect.element(screen.getByRole("button", { name: "Select Image" })).toBeInTheDocument(); for (const button of screen.getByRole("button", { name: "Save", exact: true }).all()) { await expect.element(button).toBeEnabled(); } }); it("keeps an orphaned image reference visible and removable", async () => { mockFetchSettings.mockResolvedValue({ ...defaultSettings, seo: { ...defaultSettings.seo, defaultOgImage: { mediaId: "missing-og-image", alt: "Missing social card" }, }, }); const screen = await renderSeoSettings(); await expect .element( screen.getByText( "The referenced image is no longer available. Pick a new one or remove the reference.", ), ) .toBeInTheDocument(); await expect.element(screen.getByRole("button", { name: "Change Image" })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Remove" })); await expect.element(screen.getByRole("button", { name: "Select Image" })).toBeInTheDocument(); for (const button of screen.getByRole("button", { name: "Save", exact: true }).all()) { await expect.element(button).toBeEnabled(); } await userEvent.click(screen.getByRole("button", { name: "Save", exact: true }).all()[1]); await vi.waitFor(() => { expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ seo: expect.objectContaining({ defaultOgImage: undefined }), }), ); }); }); });