Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions __tests__/atoms/atoms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/// <reference types="vitest" />
import { describe, expect, it } from "vitest";
import { createStore } from "jotai";
import {
communityStateAtom,
defaultCommunityState,
} from "@/atoms/communitiesAtom";
import { postStateAtom } from "@/atoms/postsAtom";
import { savedPostStateAtom } from "@/atoms/savedPostsAtom";
import { defaultMenuItem, directoryMenuAtom } from "@/atoms/directoryMenuAtom";
import { authModalStateAtom } from "@/atoms/authModalAtom";

describe("communityStateAtom", () => {
it("defaults to empty snippets and snippetFetched false", () => {
const store = createStore();
expect(store.get(communityStateAtom)).toEqual({
mySnippets: [],
snippetFetched: false,
});
});

it("defaultCommunityState has no currentCommunity", () => {
expect(defaultCommunityState.currentCommunity).toBeUndefined();
});

it("can be set and read back", () => {
const store = createStore();
const next = {
mySnippets: [{ communityId: "react", isAdmin: false }],
currentCommunity: { id: "react" },
snippetFetched: true,
};
store.set(communityStateAtom, next);
expect(store.get(communityStateAtom)).toEqual(next);
});
});

describe("postStateAtom", () => {
it("defaults to null selectedPost and empty arrays", () => {
const store = createStore();
expect(store.get(postStateAtom)).toEqual({
selectedPost: null,
posts: [],
postVotes: [],
});
});

it("can be set and read back", () => {
const store = createStore();
const post = { id: "p1", title: "T" };
store.set(postStateAtom, {
selectedPost: post as never,
posts: [post as never],
postVotes: [],
});
expect(store.get(postStateAtom).selectedPost).toEqual(post);
expect(store.get(postStateAtom).posts).toHaveLength(1);
});
});

describe("savedPostStateAtom", () => {
it("defaults to empty savedPosts, closed modal, not fetched", () => {
const store = createStore();
expect(store.get(savedPostStateAtom)).toEqual({
savedPosts: [],
isOpen: false,
fetched: false,
});
});

it("can toggle isOpen", () => {
const store = createStore();
store.set(savedPostStateAtom, {
savedPosts: [],
isOpen: true,
fetched: false,
});
expect(store.get(savedPostStateAtom).isOpen).toBe(true);
});
});

describe("directoryMenuAtom", () => {
it("defaults to closed menu with Home item selected", () => {
const store = createStore();
const state = store.get(directoryMenuAtom);
expect(state.isOpen).toBe(false);
expect(state.selectedMenuItem.displayText).toBe("Home");
expect(state.selectedMenuItem.link).toBe("/");
});

it("defaultMenuItem has an icon and iconColor", () => {
expect(typeof defaultMenuItem.icon).toBe("function");
expect(defaultMenuItem.iconColor).toEqual({
base: "black",
_dark: "white",
});
});
});

describe("authModalStateAtom", () => {
it("defaults to closed on login view", () => {
const store = createStore();
expect(store.get(authModalStateAtom)).toEqual({
open: false,
view: "login",
});
});

it("can open with a different view", () => {
const store = createStore();
store.set(authModalStateAtom, { open: true, view: "signup" });
expect(store.get(authModalStateAtom)).toEqual({
open: true,
view: "signup",
});
});
});
138 changes: 138 additions & 0 deletions __tests__/hooks/admin/useAddAdmin.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/// <reference types="vitest" />
import { act, renderHook } from "@testing-library/react";
import { Provider, useAtomValue, useSetAtom, createStore } from "jotai";
import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
addCommunityAdmin: vi.fn(),
}));

vi.mock("@/lib/community/addCommunityAdmin", () => ({
addCommunityAdmin: mocks.addCommunityAdmin,
}));

import { communityStateAtom } from "@/atoms/communitiesAtom";
import useAddAdmin from "@/hooks/admin/useAddAdmin";
import { Community } from "@/types/community";

const makeWrapper =
(store = createStore()) =>
({ children }: { children: ReactNode }) => (

Check failure on line 21 in __tests__/hooks/admin/useAddAdmin.test.tsx

View workflow job for this annotation

GitHub Actions / Lint - Node 22.x

Component definition is missing display name

Check failure on line 21 in __tests__/hooks/admin/useAddAdmin.test.tsx

View workflow job for this annotation

GitHub Actions / Lint - Node 20.x

Component definition is missing display name

Check failure on line 21 in __tests__/hooks/admin/useAddAdmin.test.tsx

View workflow job for this annotation

GitHub Actions / Lint - Node 24.x

Component definition is missing display name
<Provider store={store}>{children}</Provider>
);
const wrapper = makeWrapper();

const community: Community = {
id: "c1",
creatorId: "creator",
numberOfMembers: 5,
privacyType: "public",
};

function useSeed() {
const set = useSetAtom(communityStateAtom);
return (current?: Community) =>
set((prev) => ({ ...prev, currentCommunity: current }));
}

describe("useAddAdmin", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.addCommunityAdmin.mockResolvedValue(undefined);
});

it("calls the lib with community id, new uid and image url", async () => {
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await act(async () => {
await result.current.handleAddAdmin("c1", { uid: "u2" }, "img.png");
});
expect(mocks.addCommunityAdmin).toHaveBeenCalledWith("c1", "u2", "img.png");
});

it("appends the user via updateAdmins callback", async () => {
const { result } = renderHook(() => useAddAdmin(), { wrapper });
const updateAdmins = vi.fn((fn: (prev: never[]) => unknown) =>
fn([] as never),
);
await act(async () => {
await result.current.handleAddAdmin(
"c1",
{ uid: "u2" },
undefined,
updateAdmins as never,
);
});
expect(updateAdmins).toHaveBeenCalled();
});

it("does not call updateAdmins when not provided", async () => {
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await act(async () => {
await result.current.handleAddAdmin("c1", { uid: "u2" });
});
expect(mocks.addCommunityAdmin).toHaveBeenCalled();
});

it("adds the uid to currentCommunity.adminIds in the atom", async () => {
const wrapper = makeWrapper();
const seed = renderHook(() => useSeed(), { wrapper }).result.current;
await act(async () => {
seed(community);
});
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await act(async () => {
await result.current.handleAddAdmin("c1", { uid: "u2" });
});
const state = renderHook(() => useAtomValue(communityStateAtom), {
wrapper,
}).result.current;
expect(state.currentCommunity?.adminIds).toEqual(["u2"]);
});

it("preserves existing adminIds and tolerates missing adminIds", async () => {
const wrapper = makeWrapper();
const seed = renderHook(() => useSeed(), { wrapper }).result.current;
await act(async () => {
seed({ ...community, adminIds: ["a0"] });
});
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await act(async () => {
await result.current.handleAddAdmin("c1", { uid: "u2" });
});
const state = renderHook(() => useAtomValue(communityStateAtom), {
wrapper,
}).result.current;
expect(state.currentCommunity?.adminIds).toEqual(["a0", "u2"]);
});

it("propagates lib errors to the caller", async () => {
mocks.addCommunityAdmin.mockRejectedValue(new Error("denied"));
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await expect(
act(async () => {
await result.current.handleAddAdmin("c1", { uid: "u2" });
}),
).rejects.toThrow("denied");
});

it("does not mutate atom state when the lib fails", async () => {
mocks.addCommunityAdmin.mockRejectedValue(new Error("denied"));
const seed = renderHook(() => useSeed(), { wrapper }).result.current;
await act(async () => {
seed(community);
});
const { result } = renderHook(() => useAddAdmin(), { wrapper });
await act(async () => {
try {
await result.current.handleAddAdmin("c1", { uid: "u2" });
} catch {
/* expected */
}
});
const state = renderHook(() => useAtomValue(communityStateAtom), {
wrapper,
}).result.current;
expect(state.currentCommunity?.adminIds).toBeUndefined();
});
});
90 changes: 90 additions & 0 deletions __tests__/hooks/admin/useAdminList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/// <reference types="vitest" />
import { act, renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
fetchCommunityAdmins: vi.fn(),
}));

vi.mock("@/lib/community/fetchCommunityAdmins", () => ({
fetchCommunityAdmins: mocks.fetchCommunityAdmins,
}));

import useAdminList from "@/hooks/admin/useAdminList";

describe("useAdminList", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("starts with an empty list and not loading", () => {
const { result } = renderHook(() => useAdminList());
expect(result.current.admins).toEqual([]);
expect(result.current.loading).toBe(false);
});

it("loads admins and clears loading", async () => {
const admins = [{ uid: "a1" }, { uid: "a2" }];
mocks.fetchCommunityAdmins.mockResolvedValue(admins);
const { result } = renderHook(() => useAdminList());
await act(async () => {
await result.current.loadAdmins("creator", ["a1"]);
});
expect(mocks.fetchCommunityAdmins).toHaveBeenCalledWith("creator", ["a1"]);
expect(result.current.admins).toEqual(admins);
expect(result.current.loading).toBe(false);
});

it("sets loading during fetch", async () => {
let resolve!: (v: unknown) => void;
mocks.fetchCommunityAdmins.mockReturnValue(
new Promise((r) => (resolve = r)),
);
const { result } = renderHook(() => useAdminList());
const p = result.current.loadAdmins("creator");
await waitFor(() => expect(result.current.loading).toBe(true));
await act(async () => {
resolve([]);
});

await p;

expect(result.current.loading).toBe(false);
});

it("empties the list and rethrows on failure", async () => {
vi.spyOn(console, "error").mockImplementation(() => {});
mocks.fetchCommunityAdmins.mockRejectedValue(new Error("boom"));
const { result } = renderHook(() => useAdminList());
await expect(
act(async () => {
await result.current.loadAdmins("creator");
}),
).rejects.toThrow("boom");
expect(result.current.admins).toEqual([]);
expect(result.current.loading).toBe(false);
vi.restoreAllMocks();
});

it("setAdmins allows manual updates", () => {
const { result } = renderHook(() => useAdminList());
act(() => {
result.current.setAdmins([{ uid: "manual" }] as never);
});
expect(result.current.admins).toEqual([{ uid: "manual" }]);
});

it("replaces previous list on subsequent loads", async () => {
mocks.fetchCommunityAdmins
.mockResolvedValueOnce([{ uid: "a1" }])
.mockResolvedValueOnce([{ uid: "b1" }]);
const { result } = renderHook(() => useAdminList());
await act(async () => {
await result.current.loadAdmins("c1");
});
await act(async () => {
await result.current.loadAdmins("c2");
});
expect(result.current.admins).toEqual([{ uid: "b1" }]);
});
});
Loading
Loading