Email is optional so they can continue without saving; skip from Save & Exit leaves the flow instead of returning to completed. Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
7.5 KiB
TypeScript
257 lines
7.5 KiB
TypeScript
import { Suspense, useState } from "react";
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import "@testing-library/jest-dom/vitest";
|
|
import { renderWithProviders } from "../utils/test-utils";
|
|
import { useAuthModal } from "../../app/contexts/AuthModalContext";
|
|
|
|
const { navMock } = vi.hoisted(() => ({
|
|
navMock: {
|
|
pathname: "/",
|
|
searchParams: new URLSearchParams(),
|
|
replace: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({
|
|
replace: navMock.replace,
|
|
push: vi.fn(),
|
|
prefetch: vi.fn(),
|
|
refresh: vi.fn(),
|
|
back: vi.fn(),
|
|
forward: vi.fn(),
|
|
}),
|
|
usePathname: () => navMock.pathname,
|
|
useSearchParams: () => navMock.searchParams,
|
|
}));
|
|
|
|
vi.mock("../../lib/create/api", () => ({
|
|
requestMagicLink: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../app/(app)/create/utils/anonymousDraftStorage", async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<
|
|
typeof import("../../app/(app)/create/utils/anonymousDraftStorage")
|
|
>();
|
|
return {
|
|
...actual,
|
|
setTransferPendingFlag: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import { requestMagicLink } from "../../lib/create/api";
|
|
import { setTransferPendingFlag } from "../../app/(app)/create/utils/anonymousDraftStorage";
|
|
|
|
function LoginTrigger() {
|
|
const { openLogin, closeLogin } = useAuthModal();
|
|
const [leftCompleted, setLeftCompleted] = useState(false);
|
|
return (
|
|
<div>
|
|
<button type="button" onClick={() => openLogin()}>
|
|
Open login modal
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
openLogin({
|
|
variant: "saveProgress",
|
|
nextPath: "/create/community-structure?syncDraft=1",
|
|
})
|
|
}
|
|
>
|
|
Open save progress
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
openLogin({
|
|
variant: "keepRule",
|
|
nextPath: "/create/completed",
|
|
})
|
|
}
|
|
>
|
|
Open keep rule
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
openLogin({
|
|
variant: "keepRule",
|
|
nextPath: "/create/completed",
|
|
onDismiss: () => setLeftCompleted(true),
|
|
})
|
|
}
|
|
>
|
|
Open keep rule then leave
|
|
</button>
|
|
<button type="button" onClick={() => closeLogin()}>
|
|
Close from outside
|
|
</button>
|
|
{leftCompleted ? <p>Left completed</p> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
describe("AuthModalProvider (header overlay)", () => {
|
|
beforeEach(() => {
|
|
vi.mocked(requestMagicLink).mockReset();
|
|
vi.mocked(setTransferPendingFlag).mockReset();
|
|
navMock.replace.mockReset();
|
|
navMock.pathname = "/";
|
|
navMock.searchParams = new URLSearchParams();
|
|
});
|
|
|
|
it("opens blurred overlay with LoginForm when openLogin is called", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /open login modal/i }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
const backdrop = screen.getByRole("dialog").parentElement;
|
|
expect(backdrop).toHaveClass("backdrop-blur-md");
|
|
expect(
|
|
screen.getByRole("heading", { name: /log in to communityrule/i }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.queryByRole("link", { name: /back to home/i }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("closes overlay when closeLogin is called", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /open login modal/i }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
await user.click(
|
|
screen.getByRole("button", { name: /close from outside/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("saveProgress openLogin wires magicLinkNextPath and transfer flag on success", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(requestMagicLink).mockResolvedValue({ ok: true });
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: /open save progress/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.queryByRole("button", { name: /continue without saving/i }),
|
|
).not.toBeInTheDocument();
|
|
await user.type(
|
|
screen.getByRole("textbox", { name: /email address/i }),
|
|
"guest@example.com",
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: /send me a magic link/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(requestMagicLink).toHaveBeenCalledWith(
|
|
"guest@example.com",
|
|
"/create/community-structure?syncDraft=1",
|
|
undefined,
|
|
);
|
|
});
|
|
expect(setTransferPendingFlag).toHaveBeenCalled();
|
|
});
|
|
|
|
it("keepRule openLogin does not set transfer pending", async () => {
|
|
const user = userEvent.setup();
|
|
vi.mocked(requestMagicLink).mockResolvedValue({ ok: true });
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /^open keep rule$/i }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.getByRole("heading", {
|
|
name: /sign in to keep this rule on your profile/i,
|
|
}),
|
|
).toBeInTheDocument();
|
|
await user.type(
|
|
screen.getByRole("textbox", { name: /email address/i }),
|
|
"guest@example.com",
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: /send me a magic link/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(requestMagicLink).toHaveBeenCalledWith(
|
|
"guest@example.com",
|
|
"/create/completed",
|
|
undefined,
|
|
);
|
|
});
|
|
expect(setTransferPendingFlag).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keepRule continue without saving closes the overlay", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(screen.getByRole("button", { name: /^open keep rule$/i }));
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
await user.click(
|
|
screen.getByRole("button", { name: /continue without saving/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
});
|
|
expect(screen.queryByText("Left completed")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("keepRule onDismiss runs when continue without saving is pressed", async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<Suspense fallback={null}>
|
|
<LoginTrigger />
|
|
</Suspense>,
|
|
);
|
|
await user.click(
|
|
screen.getByRole("button", { name: /open keep rule then leave/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
await user.click(
|
|
screen.getByRole("button", { name: /continue without saving/i }),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
|
});
|
|
expect(screen.getByText("Left completed")).toBeInTheDocument();
|
|
});
|
|
});
|