Keep a guest community logo on review and on the published rule.

Sign-in was clearing the staged file before it uploaded, and review only read the server URL, so a chosen logo disappeared after the upload step.
This commit is contained in:
adilallo committed 2026-11-05 20:02:40 -07:00
1 parent 2348a9e94f
commit 8212423ec3
19 files changed
+780 -61

No files matched your search

@@ -0,0 +1,61 @@
import { act } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CreateFlowPendingAvatarFlush } from "../../app/(app)/create/components/CreateFlowPendingAvatarFlush";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import { flushPendingCommunityAvatar } from "../../lib/create/flushPendingCommunityAvatar";
import {
renderWithProviders as render,
screen,
} from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
vi.mock("../../lib/create/flushPendingCommunityAvatar", () => ({
flushPendingCommunityAvatar: vi.fn(),
}));
function AvatarProbe() {
const { state } = useCreateFlow();
return <div data-testid="avatar">{state.communityAvatarUrl ?? ""}</div>;
}
describe("CreateFlowPendingAvatarFlush", () => {
afterEach(() => {
vi.useRealTimers();
vi.mocked(flushPendingCommunityAvatar).mockReset();
});
it("retries a transient upload and then stores the url", async () => {
vi.useFakeTimers();
vi.mocked(flushPendingCommunityAvatar)
.mockResolvedValueOnce({ status: "failed" })
.mockResolvedValueOnce({
status: "uploaded",
url: "/api/uploads/retry",
});
render(
<>
<CreateFlowPendingAvatarFlush
sessionUser={{ id: "user-1", email: "maya@example.com" }}
sessionResolved
/>
<AvatarProbe />
</>,
);
await act(async () => {
await Promise.resolve();
});
expect(flushPendingCommunityAvatar).toHaveBeenCalledTimes(1);
expect(screen.getByTestId("avatar")).toHaveTextContent("");
await act(async () => {
await vi.advanceTimersByTimeAsync(5_000);
});
expect(flushPendingCommunityAvatar).toHaveBeenCalledTimes(2);
expect(screen.getByTestId("avatar")).toHaveTextContent(
"/api/uploads/retry",
);
});
});
@@ -0,0 +1,120 @@
import { useSearchParams } from "next/navigation";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import { PostLoginDraftTransfer } from "../../app/(app)/create/PostLoginDraftTransfer";
import {
CREATE_FLOW_ANONYMOUS_KEY,
CREATE_FLOW_TRANSFER_PENDING_KEY,
} from "../../app/(app)/create/utils/anonymousDraftStorage";
import { saveDraftToServer } from "../../lib/create/api";
import { flushPendingCommunityAvatar } from "../../lib/create/flushPendingCommunityAvatar";
import { clearPendingCommunityAvatarFile } from "../../lib/create/pendingCommunityAvatarUpload";
import {
renderWithProviders as render,
screen,
waitFor,
} from "../utils/test-utils";
import "@testing-library/jest-dom/vitest";
vi.mock("../../lib/create/api", () => ({
fetchDraftFromServer: vi.fn().mockResolvedValue(null),
saveDraftToServer: vi.fn().mockResolvedValue({ ok: true }),
}));
vi.mock("../../lib/create/flushPendingCommunityAvatar", () => ({
flushPendingCommunityAvatar: vi.fn(),
forgetFlushedCommunityAvatarUrl: vi.fn(),
}));
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
clearPendingCommunityAvatarFile: vi.fn().mockResolvedValue(undefined),
readPendingCommunityAvatarFile: vi.fn().mockResolvedValue(null),
storePendingCommunityAvatarFile: vi.fn(),
}));
function mockSearchParams(value: string) {
vi.mocked(useSearchParams).mockReturnValue(
new URLSearchParams(value) as NonNullable<
ReturnType<typeof useSearchParams>
>,
);
}
function StateProbe() {
const { state } = useCreateFlow();
return (
<div>
<span data-testid="title">{state.title ?? ""}</span>
<span data-testid="avatar">{state.communityAvatarUrl ?? ""}</span>
</div>
);
}
describe("PostLoginDraftTransfer", () => {
beforeEach(() => {
localStorage.clear();
localStorage.setItem(
CREATE_FLOW_ANONYMOUS_KEY,
JSON.stringify({ title: "Garden Club" }),
);
localStorage.setItem(CREATE_FLOW_TRANSFER_PENDING_KEY, "1");
mockSearchParams("syncDraft=1");
vi.mocked(flushPendingCommunityAvatar).mockReset();
vi.mocked(saveDraftToServer).mockClear();
vi.mocked(clearPendingCommunityAvatarFile).mockClear();
});
it("uploads the staged logo before clearing the anonymous draft", async () => {
let draftStillPresent = false;
vi.mocked(flushPendingCommunityAvatar).mockImplementation(async () => {
draftStillPresent =
localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY) != null;
return { status: "uploaded", url: "/api/uploads/from-guest" };
});
render(
<>
<PostLoginDraftTransfer
sessionUser={{ id: "user-1", email: "maya@example.com" }}
/>
<StateProbe />
</>,
);
await waitFor(() => {
expect(screen.getByTestId("avatar")).toHaveTextContent(
"/api/uploads/from-guest",
);
});
expect(draftStillPresent).toBe(true);
expect(localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
expect(saveDraftToServer).toHaveBeenCalledWith(
expect.objectContaining({
title: "Garden Club",
communityAvatarUrl: "/api/uploads/from-guest",
}),
);
expect(clearPendingCommunityAvatarFile).toHaveBeenCalled();
});
it("keeps the staged file when the upload fails", async () => {
vi.mocked(flushPendingCommunityAvatar).mockResolvedValue({
status: "failed",
});
render(
<>
<PostLoginDraftTransfer
sessionUser={{ id: "user-1", email: "maya@example.com" }}
/>
<StateProbe />
</>,
);
await waitFor(() => {
expect(screen.getByTestId("title")).toHaveTextContent("Garden Club");
});
expect(clearPendingCommunityAvatarFile).not.toHaveBeenCalled();
expect(screen.getByTestId("avatar")).toHaveTextContent("");
});
});
+37 -1
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, it, expect } from "vitest";
import { beforeEach, describe, it, expect, vi } from "vitest";
import React, { useEffect } from "react";
import {
renderWithProviders as render,
@@ -9,6 +9,13 @@ import "@testing-library/jest-dom/vitest";
import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/CommunityReviewScreen";
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import { testRouter } from "../mocks/navigation";
import { readPendingCommunityAvatarFile } from "../../lib/create/pendingCommunityAvatarUpload";
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
readPendingCommunityAvatarFile: vi.fn().mockResolvedValue(null),
storePendingCommunityAvatarFile: vi.fn(),
clearPendingCommunityAvatarFile: vi.fn(),
}));
function ReviewWithTitle({ title }: { title: string }) {
const { state, updateState } = useCreateFlow();
@@ -43,6 +50,8 @@ describe("CommunityReviewScreen", () => {
beforeEach(() => {
testRouter.replace.mockReset();
testRouter.push.mockReset();
vi.mocked(readPendingCommunityAvatarFile).mockReset();
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(null);
});
it("renders without crashing", () => {
@@ -89,6 +98,33 @@ describe("CommunityReviewScreen", () => {
expect(screen.getByText("Garden Club")).toBeInTheDocument();
});
it("shows a guest logo staged in the browser on the rule card", async () => {
const pending = new File([new Uint8Array([1, 2, 3])], "logo.png", {
type: "image/png",
});
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(pending);
Object.defineProperty(URL, "createObjectURL", {
value: vi.fn(() => "blob:pending-avatar"),
writable: true,
configurable: true,
});
Object.defineProperty(URL, "revokeObjectURL", {
value: vi.fn(),
writable: true,
configurable: true,
});
render(<ReviewWithTitle title="Garden Club" />);
await waitFor(() => {
expect(screen.getByRole("img", { name: "Garden Club" })).toHaveAttribute(
"src",
"blob:pending-avatar",
);
});
expect(readPendingCommunityAvatarFile).toHaveBeenCalled();
});
it("omits the Rule description when the user has not entered community context", () => {
render(<ReviewWithTitle title="Garden Club" />);
expect(