Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import NotificationDropReactedGroup from "@/components/brain/notifications/drop-reacted/NotificationDropReactedGroup";
import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause";
import type { ApiProfileMin } from "@/generated/models/ApiProfileMin";
import type {
GroupedReactionsItem,
INotificationDropReacted,
} from "@/types/feed.types";

const OverlappingAvatars = jest.fn(({ items }: { items: unknown[] }) => (
<div data-testid="avatars">{JSON.stringify(items)}</div>
));
const NotificationHeader = jest.fn(
({
author,
actions,
children,
}: {
author: { handle?: string | null; pfp?: string | null };
actions?: ReactNode;
children: ReactNode;
}) => (
<div data-testid="header">
<span>{author.handle}</span>
<span>{author.pfp}</span>
{children}
{actions}
</div>
)
);

jest.mock("@/components/common/OverlappingAvatars", () => ({
__esModule: true,
default: (props: { items: unknown[] }) => OverlappingAvatars(props),
}));

jest.mock("@/components/brain/notifications/NotificationsFollowAllBtn", () => ({
__esModule: true,
default: () => <div data-testid="follow-all" />,
}));

jest.mock("@/components/brain/notifications/NotificationsFollowBtn", () => ({
__esModule: true,
default: () => <div data-testid="follow-one" />,
}));

jest.mock(
"@/components/brain/notifications/subcomponents/NotificationHeader",
() => ({
__esModule: true,
default: (props: {
author: { handle?: string | null; pfp?: string | null };
actions?: ReactNode;
children: ReactNode;
}) => NotificationHeader(props),
})
);

jest.mock("@/components/brain/notifications/subcomponents/NotificationDrop", () => ({
__esModule: true,
default: () => <div data-testid="drop" />,
}));

jest.mock(
"@/components/brain/notifications/drop-reacted/ReactionEmojiPreview",
() => ({
__esModule: true,
default: ({ rawId }: { rawId: string }) => <span>{rawId}</span>,
})
);

jest.mock(
"@/components/brain/notifications/subcomponents/NotificationTimestamp",
() => ({
__esModule: true,
default: ({ createdAt }: { createdAt: number }) => (
<span>{createdAt}</span>
),
})
);

jest.mock(
"@/components/brain/notifications/utils/navigationUtils",
() => ({
__esModule: true,
getIsDirectMessage: () => false,
useWaveNavigation: () => ({
createReplyClickHandler: () => jest.fn(),
createQuoteClickHandler: () => jest.fn(),
}),
})
);

function createMockProfile(
overrides: Partial<ApiProfileMin> & { handle: string }
): ApiProfileMin {
return {
id: overrides.id ?? `${overrides.handle}-id`,
handle: overrides.handle,
pfp: overrides.pfp ?? null,
banner1_color: overrides.banner1_color ?? null,
banner2_color: overrides.banner2_color ?? null,
cic: overrides.cic ?? 0,
rep: overrides.rep ?? 0,
tdh: overrides.tdh ?? 0,
tdh_rate: overrides.tdh_rate ?? 0,
xtdh: overrides.xtdh ?? 0,
xtdh_rate: overrides.xtdh_rate ?? 0,
level: overrides.level ?? 0,
primary_address: overrides.primary_address ?? `0x${overrides.handle}`,
subscribed_actions: overrides.subscribed_actions ?? [],
archived: overrides.archived ?? false,
active_main_stage_submission_ids:
overrides.active_main_stage_submission_ids ?? [],
winner_main_stage_drop_ids: overrides.winner_main_stage_drop_ids ?? [],
artist_of_prevote_cards: overrides.artist_of_prevote_cards ?? [],
is_wave_creator: overrides.is_wave_creator ?? false,
};
}

function createMockDrop(): GroupedReactionsItem["drop"] {
return {
id: "drop-1",
wave: { id: "wave-1" },
} as GroupedReactionsItem["drop"];
}

function createNotification({
id,
createdAt,
reaction,
handle,
pfp,
}: {
id: number;
createdAt: number;
reaction: string;
handle: string;
pfp: string | null;
}): INotificationDropReacted {
return {
id,
cause: ApiNotificationCause.DropReacted,
created_at: createdAt,
read_at: null,
related_identity: createMockProfile({
handle,
pfp,
primary_address: `0x${handle}`,
}),
related_drops: [
createMockDrop(),
],
additional_context: {
reaction,
},
};
}

describe("NotificationDropReactedGroup", () => {
beforeEach(() => {
OverlappingAvatars.mockClear();
NotificationHeader.mockClear();
});

it("keeps an older pfp when the latest grouped notification for that user has none", () => {
render(
<NotificationDropReactedGroup
group={{
type: "grouped_reactions",
id: 3,
createdAt: 200,
drop: createMockDrop(),
notifications: [
createNotification({
id: 1,
createdAt: 100,
reaction: ":heart:",
handle: "gpebbles",
pfp: "alice.png",
}),
createNotification({
id: 2,
createdAt: 200,
reaction: ":heart:",
handle: "gpebbles",
pfp: null,
}),
createNotification({
id: 3,
createdAt: 150,
reaction: ":fire:",
handle: "prxt0",
pfp: "bob.png",
}),
],
}}
activeDrop={null}
onReply={jest.fn()}
/>
);

expect(screen.getByText("New reactions")).toBeInTheDocument();
expect(OverlappingAvatars).toHaveBeenCalled();
expect(OverlappingAvatars.mock.calls[0]?.[0]?.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: "gpebbles-id",
pfpUrl: "alice.png",
fallback: "GP",
}),
])
);
});

it("renders single-actor copy when deduping leaves one visible reactor", () => {
render(
<NotificationDropReactedGroup
group={{
type: "grouped_reactions",
id: 2,
createdAt: 200,
drop: createMockDrop(),
notifications: [
createNotification({
id: 1,
createdAt: 100,
reaction: ":heart:",
handle: "gpebbles",
pfp: "gpebbles.png",
}),
createNotification({
id: 2,
createdAt: 200,
reaction: ":heart:",
handle: "gpebbles",
pfp: null,
}),
],
}}
activeDrop={null}
onReply={jest.fn()}
/>
);

expect(screen.queryByText("New reactions")).not.toBeInTheDocument();
expect(screen.getByTestId("header")).toHaveTextContent("gpebbles");
expect(screen.getByTestId("header")).toHaveTextContent("gpebbles.png");
expect(screen.getByText("reacted")).toBeInTheDocument();
expect(screen.getByTestId("follow-one")).toBeInTheDocument();
expect(screen.queryByTestId("follow-all")).not.toBeInTheDocument();
});
});
99 changes: 99 additions & 0 deletions __tests__/components/header/HeaderSearchModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,105 @@ describe("HeaderSearchModal", () => {
).toBe(true);
});

it("matches close pluralized page titles for page searches", async () => {
setup({
selectedCategory: "PAGES",
sidebarSections: [
{
key: "network",
name: "Network",
icon: () => null,
items: [{ name: "Memes Calendar", href: "/meme-calendar" }],
subsections: [],
},
],
queryImpl: () => ({
isFetching: false,
data: [],
error: undefined,
refetch: jest.fn(() => Promise.resolve()),
}),
});

const input = screen.getByRole("textbox", { name: "Search" });
fireEvent.change(input, { target: { value: "meme calendar" } });

const items = await screen.findAllByTestId("item");
expect(
items.some(
(item) =>
(item.textContent ?? "").includes('"title":"Memes Calendar"') &&
(item.textContent ?? "").includes('"/meme-calendar"')
)
).toBe(true);
});

it("matches close singularized page titles for page searches", async () => {
setup({
selectedCategory: "PAGES",
sidebarSections: [
{
key: "network",
name: "Network",
icon: () => null,
items: [{ name: "Meme Calendar", href: "/meme-calendar" }],
subsections: [],
},
],
queryImpl: () => ({
isFetching: false,
data: [],
error: undefined,
refetch: jest.fn(() => Promise.resolve()),
}),
});

const input = screen.getByRole("textbox", { name: "Search" });
fireEvent.change(input, { target: { value: "memes calendar" } });

const items = await screen.findAllByTestId("item");
expect(
items.some(
(item) =>
(item.textContent ?? "").includes('"title":"Meme Calendar"') &&
(item.textContent ?? "").includes('"/meme-calendar"')
)
).toBe(true);
});

it("matches partial token page searches with close pluralization", async () => {
setup({
selectedCategory: "PAGES",
sidebarSections: [
{
key: "network",
name: "Network",
icon: () => null,
items: [{ name: "Memes Calendar", href: "/meme-calendar" }],
subsections: [],
},
],
queryImpl: () => ({
isFetching: false,
data: [],
error: undefined,
refetch: jest.fn(() => Promise.resolve()),
}),
});

const input = screen.getByRole("textbox", { name: "Search" });
fireEvent.change(input, { target: { value: "meme cal" } });

const items = await screen.findAllByTestId("item");
expect(
items.some(
(item) =>
(item.textContent ?? "").includes('"title":"Memes Calendar"') &&
(item.textContent ?? "").includes('"/meme-calendar"')
)
).toBe(true);
});

it("includes drop forge pages in search results when accessible", () => {
setup({
selectedCategory: "PAGES",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
formatUtcMonth,
formatUtcMonthYear,
mintStartInstantUtcForMintDay,
nextMintDateOnOrAfter,
wallTimeToUtcInstantInZone,
Expand Down Expand Up @@ -27,6 +29,17 @@ const mintEndInstantUtcForMintDay = (mintDay: Date): Date => {
};

describe("meme calendar timezone handling", () => {
const originalTz = process.env.TZ;

afterEach(() => {
if (originalTz === undefined) {
delete process.env.TZ;
return;
}

process.env.TZ = originalTz;
});
Comment thread
prxt6529 marked this conversation as resolved.

it("keeps mint start anchored to 17:40 Athens time across 2024", () => {
const months = Array.from({ length: 12 }, (_, idx) => idx);

Expand Down Expand Up @@ -121,4 +134,13 @@ describe("meme calendar timezone handling", () => {
expect(summerPhaseTimes[0]?.toISOString()).toBe("2024-07-01T14:40:00.000Z");
expect(winterPhaseTimes[0]?.toISOString()).toBe("2024-01-03T15:40:00.000Z");
});

it("formats UTC month labels consistently for US timezones", () => {
process.env.TZ = "America/Los_Angeles";

const seasonStart = isoDate(2026, 0, 1);

expect(formatUtcMonth(seasonStart, "long")).toBe("January");
expect(formatUtcMonthYear(seasonStart)).toBe("Jan 2026");
});
});
Loading
Loading