import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { userEvent } from "vitest/browser"; import { SeoPanel } from "../../src/components/SeoPanel"; import { render } from "../utils/render"; describe("SeoPanel", () => { beforeEach(() => { vi.useRealTimers(); }); it("debounces text field saves", async () => { const onChange = vi.fn(); const screen = await render( , ); const titleInput = screen.getByLabelText("SEO Title"); await userEvent.type(titleInput, "SEO title"); await new Promise((resolve) => setTimeout(resolve, 100)); expect(onChange).not.toHaveBeenCalled(); await vi.waitFor( () => { expect(onChange).toHaveBeenCalledTimes(1); }, { timeout: 1500 }, ); expect(onChange).toHaveBeenLastCalledWith({ title: "SEO title", description: null, canonical: null, noIndex: false, }); }); it("saves noindex changes immediately", async () => { const onChange = vi.fn(); const screen = await render( , ); await userEvent.click(screen.getByRole("switch")); await vi.waitFor(() => { expect(onChange).toHaveBeenCalledWith({ title: null, description: null, canonical: null, noIndex: true, }); }); }); it("does not overwrite newer local text when stale props arrive", async () => { function Host() { const [seo, setSeo] = React.useState({ title: "Original", description: null, image: null, canonical: null, noIndex: false, }); return ( <> {}} /> ); } const screen = await render(); const titleInput = screen.getByLabelText("SEO Title"); await userEvent.clear(titleInput); await userEvent.type(titleInput, "Newest local value"); await vi.waitFor(() => { expect((titleInput.element() as HTMLInputElement).value).toBe("Newest local value"); }); const stalePropsButton = screen.getByRole("button", { name: "Apply stale props" }); await userEvent.click(stalePropsButton); expect((titleInput.element() as HTMLInputElement).value).toBe("Newest local value"); }); it("resets when switching to a different content item", async () => { const onChange = vi.fn(); function Host() { const [contentKey, setContentKey] = React.useState("post-1"); const [seo, setSeo] = React.useState({ title: "First post", description: null, image: null, canonical: null, noIndex: false, }); return ( <> ); } const screen = await render(); const titleInput = screen.getByLabelText("SEO Title"); await userEvent.clear(titleInput); await userEvent.type(titleInput, "Unsaved local edit"); await userEvent.click(screen.getByRole("button", { name: "Switch content" })); expect(onChange).toHaveBeenCalledWith({ title: "Unsaved local edit", description: null, canonical: null, noIndex: false, }); expect((titleInput.element() as HTMLInputElement).value).toBe("Second post"); await new Promise((resolve) => setTimeout(resolve, 700)); expect(onChange).toHaveBeenCalledTimes(1); }); it("flushes pending text changes on unmount", async () => { const onChange = vi.fn(); function Host() { const [isVisible, setIsVisible] = React.useState(true); return ( <> {isVisible ? ( ) : null} ); } const screen = await render(); const titleInput = screen.getByLabelText("SEO Title"); await userEvent.type(titleInput, "SEO title"); await userEvent.click(screen.getByRole("button", { name: "Hide panel" })); expect(onChange).toHaveBeenCalledWith({ title: "SEO title", description: null, canonical: null, noIndex: false, }); await new Promise((resolve) => setTimeout(resolve, 700)); expect(onChange).toHaveBeenCalledTimes(1); }); });