diff --git a/__tests__/components/brain/BrainMobile.test.tsx b/__tests__/components/brain/BrainMobile.test.tsx
index 8a2c50d25a..22ae582b38 100644
--- a/__tests__/components/brain/BrainMobile.test.tsx
+++ b/__tests__/components/brain/BrainMobile.test.tsx
@@ -88,6 +88,12 @@ jest.mock("@/hooks/useWaveTimers", () => ({
}),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(() => ({
+ isConnected: true,
+ })),
+}));
+
jest.mock("@/components/brain/BrainDesktopDrop", () => ({
__esModule: true,
default: (props: any) => (
diff --git a/__tests__/components/brain/ContentTabContext.test.tsx b/__tests__/components/brain/ContentTabContext.test.tsx
index 12c3deea26..7dcb602656 100644
--- a/__tests__/components/brain/ContentTabContext.test.tsx
+++ b/__tests__/components/brain/ContentTabContext.test.tsx
@@ -28,6 +28,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "chat-wave",
isChatWave: true,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -44,6 +45,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "meme-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: true,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -59,12 +61,34 @@ describe("ContentTabContext", () => {
]);
});
+ it("omits My Votes for disconnected memes waves", () => {
+ const { result } = setup();
+ act(() =>
+ result.current.updateAvailableTabs({
+ waveId: "meme-wave",
+ isChatWave: false,
+ isConnected: false,
+ isMemesWave: true,
+ isCurationWave: false,
+ votingState: WaveVotingState.NOT_STARTED,
+ hasFirstDecisionPassed: false,
+ })
+ );
+ expect(result.current.availableTabs).toEqual([
+ MyStreamWaveTab.LEADERBOARD,
+ MyStreamWaveTab.CHAT,
+ MyStreamWaveTab.OUTCOME,
+ MyStreamWaveTab.FAQ,
+ ]);
+ });
+
it("defaults to LEADERBOARD for memes waves", () => {
const { result } = setup();
act(() =>
result.current.updateAvailableTabs({
waveId: "meme-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: true,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -80,6 +104,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "default-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -95,6 +120,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "curation-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: true,
votingState: WaveVotingState.NOT_STARTED,
@@ -116,6 +142,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "meme-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: true,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -127,6 +154,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "default-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -137,6 +165,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "meme-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: true,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -152,6 +181,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "default-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -163,6 +193,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "default-wave",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -179,6 +210,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "wave-1",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: false,
votingState: WaveVotingState.NOT_STARTED,
@@ -191,6 +223,7 @@ describe("ContentTabContext", () => {
result.current.updateAvailableTabs({
waveId: "wave-1",
isChatWave: false,
+ isConnected: true,
isMemesWave: false,
isCurationWave: true,
votingState: WaveVotingState.NOT_STARTED,
diff --git a/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx b/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx
index 003ec1d17a..c6c0d1a83e 100644
--- a/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx
+++ b/__tests__/components/brain/mobile/BrainMobileTabs.test.tsx
@@ -52,6 +52,10 @@ jest.mock("@/components/auth/Auth", () => ({
useAuth: () => ({ connectedProfile: { handle: "alice" } }),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
+
const leaderboardMock = jest.fn();
jest.mock("@/components/brain/my-stream/MyStreamWaveTabsLeaderboard", () => ({
__esModule: true,
@@ -71,6 +75,9 @@ jest.mock("@/components/brain/my-stream/MyStreamWaveTabsLeaderboard", () => ({
const { useWave } = require("@/hooks/useWave");
const { useUnreadIndicator } = require("@/hooks/useUnreadIndicator");
const { useUnreadNotifications } = require("@/hooks/useUnreadNotifications");
+const {
+ useSeizeConnectContext,
+} = require("@/components/auth/SeizeConnectContext");
describe("BrainMobileTabs", () => {
const onViewChange = jest.fn();
@@ -85,6 +92,9 @@ describe("BrainMobileTabs", () => {
(useUnreadNotifications as jest.Mock).mockReturnValue({
haveUnreadNotifications: false,
});
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ isConnected: true,
+ });
});
it("renders back button and navigates to My Stream", async () => {
@@ -203,6 +213,34 @@ describe("BrainMobileTabs", () => {
expect(screen.queryByText("FAQ")).toBeNull();
});
+ it("hides My Votes for disconnected memes rank wave", () => {
+ (useWave as jest.Mock).mockReturnValue({
+ isMemesWave: true,
+ isCurationWave: false,
+ isRankWave: true,
+ });
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ isConnected: false,
+ });
+
+ render(
+
+ );
+
+ expect(screen.getByTestId("leaderboard")).toBeInTheDocument();
+ expect(screen.queryByText("My Votes")).toBeNull();
+ expect(screen.getByText("Outcome")).toBeInTheDocument();
+ expect(screen.getByText("FAQ")).toBeInTheDocument();
+ });
+
it("renders Sales for non-rank curation waves", () => {
(useWave as jest.Mock).mockReturnValue({
isMemesWave: false,
diff --git a/__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx b/__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
index 8a4484b30f..b87294f6ba 100644
--- a/__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
+++ b/__tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
@@ -17,6 +17,7 @@ const mockRemoveAllDeliveredNotifications = jest
.fn()
.mockResolvedValue(undefined);
const invalidateNotificationsMock = jest.fn();
+const mockUseAuth = jest.fn();
jest.mock("next/navigation", () => ({
useRouter: () => ({ replace: replaceMock }),
@@ -91,6 +92,10 @@ jest.mock("@/components/notifications/NotificationsContext", () => ({
}),
}));
+jest.mock("@/components/auth/Auth", () => ({
+ useAuth: () => mockUseAuth(),
+}));
+
jest.mock("@/services/api/common-api", () => ({
commonApiPostWithoutBodyAndResponse: jest.fn().mockResolvedValue(undefined),
}));
@@ -114,6 +119,9 @@ describe("MyStreamWaveChat", () => {
mockRemoveWaveDeliveredNotifications.mockClear();
mockRemoveAllDeliveredNotifications.mockClear();
invalidateNotificationsMock.mockClear();
+ mockUseAuth.mockReturnValue({
+ connectedProfile: { handle: "tester" },
+ });
(
commonApiPostWithoutBodyAndResponse as jest.MockedFunction<
typeof commonApiPostWithoutBodyAndResponse
@@ -196,4 +204,27 @@ describe("MyStreamWaveChat", () => {
expect(invalidateNotificationsMock).toHaveBeenCalled();
});
});
+
+ it("skips notification cleanup on unmount for anonymous viewers", async () => {
+ mockUseAuth.mockReturnValue({
+ connectedProfile: null,
+ });
+
+ const { unmount } = renderWithProvider(
+
+ );
+
+ await act(async () => {
+ unmount();
+ });
+
+ expect(mockRemoveWaveDeliveredNotifications).not.toHaveBeenCalled();
+ expect(commonApiPostWithoutBodyAndResponse).not.toHaveBeenCalled();
+ expect(invalidateNotificationsMock).not.toHaveBeenCalled();
+ });
});
diff --git a/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx b/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx
index 3388d36569..0c50a4595e 100644
--- a/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx
+++ b/__tests__/components/brain/my-stream/MyStreamWaveDesktopTabs.test.tsx
@@ -60,6 +60,10 @@ jest.mock("@/components/waves/leaderboard/time/CompactTimeCountdown", () => ({
),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
+
import MyStreamWaveDesktopTabs from "@/components/brain/my-stream/MyStreamWaveDesktopTabs";
let mockAvailableTabs: MyStreamWaveTab[] = [];
@@ -71,6 +75,9 @@ let mockWaveInfo: any = {
};
let mockVoting = { isUpcoming: false, isCompleted: false, isInProgress: true };
let mockDecisions: { timestamp: number }[] = [];
+const {
+ useSeizeConnectContext,
+} = require("@/components/auth/SeizeConnectContext");
function renderComponent(activeTab: MyStreamWaveTab = MyStreamWaveTab.CHAT) {
return render(
@@ -93,6 +100,9 @@ beforeEach(() => {
};
mockVoting = { isUpcoming: false, isCompleted: false, isInProgress: true };
mockDecisions = [];
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ isConnected: true,
+ });
});
describe("MyStreamWaveDesktopTabs", () => {
@@ -146,6 +156,32 @@ describe("MyStreamWaveDesktopTabs", () => {
expect(setActiveTab).not.toHaveBeenCalled();
});
+ it("hides My Votes for disconnected memes waves", () => {
+ mockWaveInfo = {
+ isChatWave: false,
+ isMemesWave: true,
+ isCurationWave: false,
+ isRankWave: false,
+ };
+ mockAvailableTabs = [
+ MyStreamWaveTab.LEADERBOARD,
+ MyStreamWaveTab.CHAT,
+ MyStreamWaveTab.MY_VOTES,
+ MyStreamWaveTab.OUTCOME,
+ MyStreamWaveTab.FAQ,
+ ];
+ (useSeizeConnectContext as jest.Mock).mockReturnValue({
+ isConnected: false,
+ });
+
+ renderComponent(MyStreamWaveTab.MY_VOTES);
+
+ expect(screen.getByText("Leaderboard")).toBeInTheDocument();
+ expect(screen.getByText("Chat")).toBeInTheDocument();
+ expect(screen.queryByText("My Votes")).toBeNull();
+ expect(setActiveTab).toHaveBeenCalledWith(MyStreamWaveTab.LEADERBOARD);
+ });
+
it("keeps Sales hidden outside curation waves", () => {
mockWaveInfo = {
isChatWave: false,
diff --git a/__tests__/components/drops/view/DropsList.test.tsx b/__tests__/components/drops/view/DropsList.test.tsx
index 436626942b..3392f73d0e 100644
--- a/__tests__/components/drops/view/DropsList.test.tsx
+++ b/__tests__/components/drops/view/DropsList.test.tsx
@@ -235,6 +235,20 @@ describe("DropsList", () => {
rank: 1,
variant: "chat",
});
+
+ const boostedCard = screen.getByTestId("boosted-card");
+
+ expect(boostedCard.parentElement).toHaveClass(
+ "tw-rounded-2xl",
+ "tw-bg-iron-900/50",
+ "tw-p-2",
+ "sm:tw-p-3"
+ );
+ expect(boostedCard.parentElement?.parentElement).toHaveClass(
+ "tw-px-3",
+ "tw-py-4",
+ "sm:tw-px-4"
+ );
});
it("clicking an inline boosted card calls onBoostedDropClick with the drop serial", () => {
diff --git a/__tests__/components/home/boosted/BoostedDropCardHome.test.tsx b/__tests__/components/home/boosted/BoostedDropCardHome.test.tsx
index 6eeed46155..a9aca8c1b5 100644
--- a/__tests__/components/home/boosted/BoostedDropCardHome.test.tsx
+++ b/__tests__/components/home/boosted/BoostedDropCardHome.test.tsx
@@ -446,6 +446,35 @@ describe("BoostedDropCardHome", () => {
variant: "chat",
})
);
+
+ expect(screen.getByTestId("link-preview").parentElement).toHaveClass(
+ "tw-px-3",
+ "sm:tw-px-4"
+ );
+ });
+
+ it("adds bottom spacing to preview-only chat cards", () => {
+ renderWithAuth(
+
+ );
+
+ expect(screen.getByTestId("link-preview").parentElement).toHaveClass(
+ "tw-px-3",
+ "sm:tw-px-4",
+ "tw-pb-4"
+ );
});
it("keeps caption content for chat cards with lead media", () => {
@@ -587,6 +616,9 @@ describe("BoostedDropCardHome", () => {
/>
);
+ expect(
+ screen.getByTestId("boosted-drop-media-frame").parentElement
+ ).toHaveClass("tw-px-3", "sm:tw-px-4", "tw-pb-4");
expect(screen.getByTestId("boosted-drop-media-frame")).toHaveStyle({
aspectRatio: "8 / 5",
});
diff --git a/__tests__/components/user/utils/profile/UserProfileTooltip.test.tsx b/__tests__/components/user/utils/profile/UserProfileTooltip.test.tsx
index 95ffaf2556..474ebf8359 100644
--- a/__tests__/components/user/utils/profile/UserProfileTooltip.test.tsx
+++ b/__tests__/components/user/utils/profile/UserProfileTooltip.test.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import type { ComponentProps, ContextType } from "react";
import { AuthContext } from "@/components/auth/Auth";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import UserProfileTooltip from "@/components/user/utils/profile/UserProfileTooltip";
import type { CicStatement } from "@/entities/IProfile";
import { STATEMENT_GROUP, STATEMENT_TYPE } from "@/helpers/Types";
@@ -122,6 +123,10 @@ jest.mock("@/hooks/useDeviceInfo", () => ({
default: jest.fn(() => ({ isApp: false })),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
+
jest.mock("@/services/api/common-api", () => ({
commonApiFetch: jest.fn(),
}));
@@ -139,6 +144,7 @@ jest.mock("next/navigation", () => ({
}));
const useIdentityMock = useIdentity as jest.Mock;
+const useSeizeConnectContextMock = useSeizeConnectContext as jest.Mock;
const commonApiFetchMock = commonApiFetch as jest.Mock;
const createDirectMessageWaveMock = createDirectMessageWave as jest.Mock;
const navigateToDirectMessageMock = navigateToDirectMessage as jest.Mock;
@@ -220,6 +226,7 @@ describe("UserProfileTooltip", () => {
useRouterMock.mockReturnValue(mockRouter);
useIdentityMock.mockReturnValue({ profile: mockProfile });
+ useSeizeConnectContextMock.mockReturnValue({ isConnected: true });
createDirectMessageWaveMock.mockResolvedValue({ id: "wave-1" });
commonApiFetchMock.mockImplementation(
@@ -367,6 +374,21 @@ describe("UserProfileTooltip", () => {
).not.toBeInTheDocument();
});
+ it("does not render the follow action when logged out", () => {
+ useSeizeConnectContextMock.mockReturnValue({ isConnected: false });
+
+ renderTooltip({
+ authOverrides: {
+ connectedProfile: null,
+ },
+ });
+
+ expect(screen.queryByTestId("follow-btn")).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole("button", { name: "Send direct message" })
+ ).not.toBeInTheDocument();
+ });
+
it("hides the DM action when acting through a proxy", () => {
renderTooltip({
authOverrides: {
diff --git a/__tests__/components/waves/DropPlaceholder.test.tsx b/__tests__/components/waves/DropPlaceholder.test.tsx
index 8f8457abe4..9d8f1842af 100644
--- a/__tests__/components/waves/DropPlaceholder.test.tsx
+++ b/__tests__/components/waves/DropPlaceholder.test.tsx
@@ -1,248 +1,291 @@
-import DropPlaceholder from '@/components/waves/DropPlaceholder';
-import { ChatRestriction, SubmissionRestriction } from '@/hooks/useDropPriviledges';
-import { render, screen } from '@testing-library/react';
+import DropPlaceholder from "@/components/waves/DropPlaceholder";
+import {
+ ChatRestriction,
+ SubmissionRestriction,
+} from "@/hooks/useDropPriviledges";
+import { render, screen } from "@testing-library/react";
-describe('DropPlaceholder', () => {
- describe('chat restrictions', () => {
- it('renders not logged in message for chat', () => {
+describe("DropPlaceholder", () => {
+ describe("chat restrictions", () => {
+ it("renders not logged in message for chat", () => {
render(
-
);
-
- expect(screen.getByText('Please log in to participate in chat')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Please log in to participate in chat")
+ ).toBeInTheDocument();
});
- it('renders proxy user message for chat', () => {
+ it("renders proxy user message for chat", () => {
render(
-
);
-
- expect(screen.getByText('Proxy users cannot participate in chat')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Proxy users cannot participate in chat")
+ ).toBeInTheDocument();
});
- it('renders no permission message for chat', () => {
+ it("renders no permission message for chat", () => {
render(
-
);
-
- expect(screen.getByText("You don't have permission to chat in this wave")).toBeInTheDocument();
+
+ expect(
+ screen.getByText("You don't have permission to chat in this wave")
+ ).toBeInTheDocument();
});
- it('renders disabled message for chat', () => {
+ it("renders disabled message for chat", () => {
render(
-
);
-
- expect(screen.getByText('Chat is currently disabled for this wave')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Chat is currently disabled for this wave")
+ ).toBeInTheDocument();
});
- it('applies primary color for not logged in chat restriction', () => {
+ it("applies primary color for not logged in chat restriction", () => {
render(
-
);
-
- const message = screen.getByText('Please log in to participate in chat');
- expect(message).toHaveClass('tw-text-primary-400');
+
+ const message = screen.getByText("Please log in to participate in chat");
+ expect(message).toHaveClass("tw-text-primary-400");
});
});
- describe('submission restrictions', () => {
- it('renders not logged in message for submission', () => {
+ describe("submission restrictions", () => {
+ it("renders not logged in message for submission", () => {
render(
-
);
-
- expect(screen.getByText('Please log in to make submissions')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Please log in to make submissions")
+ ).toBeInTheDocument();
});
- it('renders proxy user message for submission', () => {
+ it("renders proxy user message for submission", () => {
render(
-
);
-
- expect(screen.getByText('Proxy users cannot make submissions')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Proxy users cannot make submissions")
+ ).toBeInTheDocument();
});
- it('renders no permission message for submission', () => {
+ it("renders no permission message for submission", () => {
render(
-
);
-
- expect(screen.getByText("You don't have permission to submit in this wave")).toBeInTheDocument();
+
+ expect(
+ screen.getByText("You don't have permission to submit in this wave")
+ ).toBeInTheDocument();
});
- it('renders not started message for submission', () => {
+ it("renders not started message for submission", () => {
render(
-
);
-
- expect(screen.getByText("Submissions haven't started yet")).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Submissions haven't started yet")
+ ).toBeInTheDocument();
});
- it('renders ended message for submission', () => {
+ it("renders ended message for submission", () => {
render(
-
);
-
- expect(screen.getByText('Submission period has ended')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("Submission period has ended")
+ ).toBeInTheDocument();
});
- it('renders max drops reached message for submission', () => {
+ it("renders max drops reached message for submission", () => {
render(
-
);
-
- expect(screen.getByText('You have reached the maximum number of drops allowed')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("You have reached the maximum number of drops allowed")
+ ).toBeInTheDocument();
});
- it('applies correct colors for submission restrictions', () => {
+ it("applies correct colors for submission restrictions", () => {
const { rerender } = render(
-
);
-
- expect(screen.getByText('Please log in to make submissions')).toHaveClass('tw-text-primary-400');
+
+ expect(screen.getByText("Please log in to make submissions")).toHaveClass(
+ "tw-text-primary-400"
+ );
rerender(
-
);
-
- expect(screen.getByText("Submissions haven't started yet")).toHaveClass('tw-text-[#FEDF89]');
+
+ expect(screen.getByText("Submissions haven't started yet")).toHaveClass(
+ "tw-text-[#FEDF89]"
+ );
rerender(
-
);
-
- expect(screen.getByText('Submission period has ended')).toHaveClass('tw-text-red');
+
+ expect(screen.getByText("Submission period has ended")).toHaveClass(
+ "tw-text-red"
+ );
rerender(
-
);
-
- expect(screen.getByText('You have reached the maximum number of drops allowed')).toHaveClass('tw-text-red');
+
+ expect(
+ screen.getByText("You have reached the maximum number of drops allowed")
+ ).toHaveClass("tw-text-red");
});
});
- describe('both type', () => {
- it('renders generic message for both type', () => {
+ describe("both type", () => {
+ it("renders connect wallet message for logged out users", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText("Connect your wallet to participate in this wave")
+ ).toBeInTheDocument();
+ });
+
+ it("renders generic message for both type", () => {
render();
-
- expect(screen.getByText('You cannot participate in this wave at the moment')).toBeInTheDocument();
+
+ expect(
+ screen.getByText("You cannot participate in this wave at the moment")
+ ).toBeInTheDocument();
});
});
- describe('default cases', () => {
- it('renders default message when no restrictions provided', () => {
+ describe("default cases", () => {
+ it("renders default message when no restrictions provided", () => {
render();
-
- expect(screen.getByText('Action not available')).toBeInTheDocument();
+
+ expect(screen.getByText("Action not available")).toBeInTheDocument();
});
- it('applies default neutral color', () => {
+ it("applies default neutral color", () => {
render();
-
- const message = screen.getByText('Action not available');
- expect(message).toHaveClass('tw-text-iron-400');
+
+ const message = screen.getByText("Action not available");
+ expect(message).toHaveClass("tw-text-iron-400");
});
});
- describe('component structure', () => {
- it('renders with correct container classes', () => {
+ describe("component structure", () => {
+ it("renders with correct container classes", () => {
const { container } = render();
-
+
const wrapper = container.firstChild;
expect(wrapper).toHaveClass(
- 'tw-min-h-[48px]',
- 'tw-flex',
- 'tw-items-center',
- 'tw-justify-center',
- 'tw-px-4',
- 'tw-py-3',
- 'tw-bg-iron-900/50',
- 'tw-backdrop-blur',
- 'tw-rounded-xl',
- 'tw-border',
- 'tw-border-iron-800/50'
+ "tw-min-h-[48px]",
+ "tw-flex",
+ "tw-items-center",
+ "tw-justify-center",
+ "tw-px-4",
+ "tw-py-3",
+ "tw-bg-iron-900/50",
+ "tw-backdrop-blur",
+ "tw-rounded-xl",
+ "tw-border",
+ "tw-border-iron-800/50"
);
});
- it('renders message with correct text styling', () => {
+ it("renders message with correct text styling", () => {
render();
-
- const message = screen.getByText('Action not available');
- expect(message).toHaveClass(
- 'tw-text-sm',
- 'tw-font-medium',
- 'tw-mb-0'
- );
+
+ const message = screen.getByText("Action not available");
+ expect(message).toHaveClass("tw-text-sm", "tw-font-medium", "tw-mb-0");
});
});
- describe('error handling', () => {
- it('throws error for unhandled chat restriction', () => {
+ describe("error handling", () => {
+ it("throws error for unhandled chat restriction", () => {
expect(() => {
render(
-
);
- }).toThrow('Unhandled chat restriction: INVALID_RESTRICTION');
+ }).toThrow("Unhandled chat restriction: INVALID_RESTRICTION");
});
- it('throws error for unhandled submission restriction', () => {
+ it("throws error for unhandled submission restriction", () => {
expect(() => {
render(
-
);
- }).toThrow('Unhandled submission restriction: INVALID_RESTRICTION');
+ }).toThrow("Unhandled submission restriction: INVALID_RESTRICTION");
});
});
-});
\ No newline at end of file
+});
diff --git a/__tests__/components/waves/WavesLayout.test.tsx b/__tests__/components/waves/WavesLayout.test.tsx
new file mode 100644
index 0000000000..5dd7f9a342
--- /dev/null
+++ b/__tests__/components/waves/WavesLayout.test.tsx
@@ -0,0 +1,141 @@
+import WavesLayout from "@/components/waves/layout/WavesLayout";
+import { render, screen } from "@testing-library/react";
+import React from "react";
+
+const mockUseAuthenticatedContent = jest.fn();
+const mockUseDeviceInfo = jest.fn();
+const mockGetActiveWaveIdFromUrl = jest.fn();
+const mockUsePathname = jest.fn();
+const mockUseSearchParams = jest.fn();
+
+jest.mock("../../../hooks/useAuthenticatedContent", () => ({
+ useAuthenticatedContent: () => mockUseAuthenticatedContent(),
+}));
+
+jest.mock("../../../hooks/useDeviceInfo", () => ({
+ __esModule: true,
+ default: () => mockUseDeviceInfo(),
+}));
+
+jest.mock("next/navigation", () => ({
+ usePathname: () => mockUsePathname(),
+ useSearchParams: () => mockUseSearchParams(),
+}));
+
+jest.mock("../../../helpers/navigation.helpers", () => ({
+ getActiveWaveIdFromUrl: (...args: unknown[]) =>
+ mockGetActiveWaveIdFromUrl(...args),
+}));
+
+jest.mock("@/components/waves/WavesDesktop", () => ({
+ __esModule: true,
+ default: ({
+ children,
+ showLeftSidebar,
+ }: {
+ readonly children: React.ReactNode;
+ readonly showLeftSidebar?: boolean;
+ }) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/waves/WavesMobile", () => ({
+ __esModule: true,
+ default: ({ children }: { readonly children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
+jest.mock("@/components/common/ConnectWallet", () => ({
+ __esModule: true,
+ default: () => Connect Wallet
,
+}));
+
+jest.mock("@/components/header/user/HeaderUserConnect", () => ({
+ __esModule: true,
+ default: ({ label }: { readonly label?: string }) => (
+
+ ),
+}));
+
+jest.mock("@/components/user/utils/set-up-profile/UserSetUpProfileCta", () => ({
+ __esModule: true,
+ default: () => Set up profile
,
+}));
+
+jest.mock("@/components/waves/WaveScreenMessage", () => ({
+ __esModule: true,
+ default: ({
+ action,
+ description,
+ title,
+ }: {
+ readonly action?: React.ReactNode;
+ readonly description?: string;
+ readonly title: string;
+ }) => (
+
+
{title}
+ {description ?
{description}
: null}
+ {action}
+
+ ),
+}));
+
+describe("WavesLayout", () => {
+ beforeEach(() => {
+ mockUseAuthenticatedContent.mockReturnValue({
+ contentState: "not-authenticated",
+ });
+ mockUseDeviceInfo.mockReturnValue({ isApp: false, isMobileDevice: false });
+ mockUsePathname.mockReturnValue("/waves/test-wave");
+ mockUseSearchParams.mockReturnValue(new URLSearchParams("wave=test-wave"));
+ mockGetActiveWaveIdFromUrl.mockReturnValue(null);
+ });
+
+ it("renders the selected wave content for logged-out users", () => {
+ mockGetActiveWaveIdFromUrl.mockReturnValue("test-wave");
+
+ render(
+
+ Real wave content
+
+ );
+
+ expect(screen.getByTestId("wave-content")).toBeInTheDocument();
+ expect(screen.getByTestId("waves-desktop")).toHaveAttribute(
+ "data-allow-drop-overlay",
+ "false"
+ );
+ expect(screen.getByTestId("waves-desktop")).toHaveAttribute(
+ "data-allow-right-sidebar",
+ "false"
+ );
+ expect(screen.queryByTestId("wave-screen-message")).not.toBeInTheDocument();
+ });
+
+ it("keeps the select-wave prompt when no wave is selected", () => {
+ render(
+
+ Real wave content
+
+ );
+
+ expect(screen.getByText("Select a Wave")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Connect your wallet to access waves and join the conversation."
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Connect Wallet" })
+ ).toBeInTheDocument();
+ expect(screen.queryByTestId("wave-content")).not.toBeInTheDocument();
+ });
+});
diff --git a/__tests__/components/waves/drops/WaveDropActions.test.tsx b/__tests__/components/waves/drops/WaveDropActions.test.tsx
index 7a8403d9ad..9f0e950318 100644
--- a/__tests__/components/waves/drops/WaveDropActions.test.tsx
+++ b/__tests__/components/waves/drops/WaveDropActions.test.tsx
@@ -1,5 +1,6 @@
import { fireEvent, render, screen } from "@testing-library/react";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
import { ApiDropType } from "@/generated/models/ApiDropType";
import WaveDropActions from "@/components/waves/drops/WaveDropActions";
@@ -14,6 +15,11 @@ jest.mock("@/components/waves/drops/WaveDropActionsBoost", () => ({
default: () => ,
}));
+jest.mock("@/components/waves/drops/WaveDropActionsCopyLink", () => ({
+ __esModule: true,
+ default: () => ,
+}));
+
jest.mock("@/components/waves/drops/WaveDropActionsEdit", () => ({
__esModule: true,
default: () => ,
@@ -55,7 +61,12 @@ jest.mock("@/contexts/SeizeSettingsContext", () => ({
useSeizeSettings: jest.fn(),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
+
const settingsMock = useSeizeSettings as jest.Mock;
+const seizeConnectContextMock = useSeizeConnectContext as jest.Mock;
const baseDrop: any = {
id: "drop-1",
@@ -66,6 +77,7 @@ const baseDrop: any = {
describe("WaveDropActions", () => {
beforeEach(() => {
settingsMock.mockReturnValue({ isMemesWave: () => false });
+ seizeConnectContextMock.mockReturnValue({ isConnected: true });
});
it("keeps hidden actions non-interactive while closed", () => {
@@ -118,4 +130,22 @@ describe("WaveDropActions", () => {
expect(screen.queryByTestId("rate")).toBeNull();
});
+
+ it("shows only copy link when disconnected", () => {
+ seizeConnectContextMock.mockReturnValue({ isConnected: false });
+
+ render(
+ {}} />
+ );
+
+ expect(screen.getByTestId("copy-link")).toBeInTheDocument();
+ expect(screen.queryByTestId("quick-react")).toBeNull();
+ expect(screen.queryByTestId("add-reaction")).toBeNull();
+ expect(screen.queryByTestId("reply")).toBeNull();
+ expect(screen.queryByTestId("boost")).toBeNull();
+ expect(
+ screen.queryByRole("button", { name: "Open more actions" })
+ ).toBeNull();
+ expect(screen.queryByTestId("rate")).toBeNull();
+ });
});
diff --git a/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx b/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
index f53fa42673..21c78b5816 100644
--- a/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
+++ b/__tests__/components/waves/drops/WaveDropMobileMenu.test.tsx
@@ -1,4 +1,5 @@
import { AuthContext } from "@/components/auth/Auth";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import WaveDropMobileMenu from "@/components/waves/drops/WaveDropMobileMenu";
import { ApiDropType } from "@/generated/models/ApiDropType";
import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
@@ -11,6 +12,9 @@ const writeText = jest.fn().mockResolvedValue(undefined);
jest.mock("@/hooks/drops/useDropInteractionRules", () => ({
useDropInteractionRules: jest.fn(),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(),
+}));
jest.mock("@/components/waves/drops/WaveDropMobileMenuDelete", () => () => (
));
@@ -71,10 +75,12 @@ beforeAll(() => {
});
const mockedUseDropInteractionRules = jest.mocked(useDropInteractionRules);
+const mockedUseSeizeConnectContext = jest.mocked(useSeizeConnectContext);
beforeEach(() => {
writeText.mockClear();
mockIsMemesWave.mockReturnValue(false);
+ mockedUseSeizeConnectContext.mockReturnValue({ isConnected: true } as any);
mockedUseDropInteractionRules.mockReturnValue({
canShowVote: true,
canVote: true,
@@ -282,3 +288,47 @@ test("does not show pinned-drop action in the mobile menu for non-admins", () =>
expect(screen.queryByTestId("set-pinned-drop")).toBeNull();
});
+
+test("shows only copy link in the mobile menu when disconnected", () => {
+ mockedUseSeizeConnectContext.mockReturnValue({ isConnected: false } as any);
+
+ const drop = {
+ id: "1",
+ serial_no: 1,
+ wave: { id: "w" },
+ drop_type: ApiDropType.Chat,
+ author: { handle: "alice" },
+ } as any;
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByText("Copy link")).toBeInTheDocument();
+ expect(screen.queryByTestId("quick-react")).toBeNull();
+ expect(screen.queryByTestId("add-reaction")).toBeNull();
+ expect(screen.queryByText("Reply")).toBeNull();
+ expect(screen.queryByTestId("boost")).toBeNull();
+ expect(screen.queryByTestId("open")).toBeNull();
+ expect(screen.queryByTestId("mark-unread")).toBeNull();
+ expect(screen.queryByTestId("clap")).toBeNull();
+ expect(screen.queryByTestId("set-pinned-drop")).toBeNull();
+ expect(screen.queryByTestId("delete")).toBeNull();
+});
diff --git a/__tests__/components/waves/drops/WaveDropReactions.test.tsx b/__tests__/components/waves/drops/WaveDropReactions.test.tsx
index aa50ba5261..eacad22659 100644
--- a/__tests__/components/waves/drops/WaveDropReactions.test.tsx
+++ b/__tests__/components/waves/drops/WaveDropReactions.test.tsx
@@ -1,4 +1,5 @@
import WaveDropReactions from "@/components/waves/drops/WaveDropReactions";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { useEmoji } from "@/contexts/EmojiContext";
import * as commonApi from "@/services/api/common-api";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
@@ -14,6 +15,10 @@ jest.mock("@/contexts/EmojiContext", () => ({
useEmoji: jest.fn(),
}));
+jest.mock("@/components/auth/SeizeConnectContext", () => ({
+ useSeizeConnectContext: jest.fn(() => ({ isConnected: true })),
+}));
+
jest.mock("@/helpers/Helpers", () => ({
formatLargeNumber: (num: number) => `${num}`,
}));
@@ -42,6 +47,7 @@ jest.mock("@/hooks/useLongPressInteraction", () => ({
}));
const mockUseEmoji = useEmoji as jest.Mock;
+const mockUseSeizeConnectContext = useSeizeConnectContext as jest.Mock;
type NativeEmojiMock = { skins: Array<{ native: string }> };
@@ -87,6 +93,7 @@ describe("WaveDropReactions", () => {
beforeEach(() => {
// Reset call history without removing default implementations
jest.clearAllMocks();
+ mockUseSeizeConnectContext.mockReturnValue({ isConnected: true });
getMyStreamMock().mockReturnValue({
applyOptimisticDropUpdate: jest.fn(() => ({ rollback: jest.fn() })),
});
@@ -247,12 +254,66 @@ describe("WaveDropReactions", () => {
await waitFor(() => {
expect(button).toHaveTextContent("3");
});
+ expect(commonApi.commonApiPost).toHaveBeenCalledWith({
+ endpoint: "drops/test-drop/reaction",
+ body: { reaction: ":gm:" },
+ });
// Click button again to decrement
fireEvent.click(button);
await waitFor(() => {
expect(button).toHaveTextContent("2");
});
+ expect(commonApi.commonApiDelete).toHaveBeenCalledWith({
+ endpoint: "drops/test-drop/reaction",
+ });
+ });
+
+ it("renders reaction pills as non-interactive when disconnected", () => {
+ mockUseSeizeConnectContext.mockReturnValue({ isConnected: false });
+ mockUseEmoji.mockReturnValue(
+ createEmojiContextValue(
+ [
+ {
+ category: "people",
+ emojis: [{ id: "gm", skins: [{ src: "/gm.png" }] }],
+ },
+ ],
+ () => null
+ )
+ );
+
+ render(
+
+ );
+
+ const button = screen.getByRole("button");
+ expect(button).toBeDisabled();
+ expect(button).not.toHaveAttribute("data-tooltip-id");
+ expect(button).toHaveTextContent("4");
+
+ fireEvent.click(button);
+
+ expect(button).toHaveTextContent("4");
+ expect(commonApi.commonApiPost).not.toHaveBeenCalled();
+ expect(commonApi.commonApiDelete).not.toHaveBeenCalled();
+ expect(screen.queryByText("Reactions")).not.toBeInTheDocument();
});
it("shows 'and X more' in tooltip when more than 3 profiles", async () => {
diff --git a/__tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx b/__tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx
new file mode 100644
index 0000000000..82d6de3319
--- /dev/null
+++ b/__tests__/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.test.tsx
@@ -0,0 +1,85 @@
+import { ReactQueryWrapperContext } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import { useWaveDropsNotificationRead } from "@/components/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead";
+import { commonApiPostWithoutBodyAndResponse } from "@/services/api/common-api";
+import { render, waitFor } from "@testing-library/react";
+import React from "react";
+
+jest.mock("@/services/api/common-api", () => ({
+ commonApiPostWithoutBodyAndResponse: jest.fn().mockResolvedValue(undefined),
+}));
+
+function TestComponent({
+ enabled,
+ removeWaveDeliveredNotifications,
+ waveId,
+}: {
+ readonly enabled?: boolean;
+ readonly removeWaveDeliveredNotifications: (
+ waveId: string
+ ) => Promise | void;
+ readonly waveId: string;
+}) {
+ useWaveDropsNotificationRead({
+ waveId,
+ enabled,
+ removeWaveDeliveredNotifications,
+ });
+
+ return null;
+}
+
+describe("useWaveDropsNotificationRead", () => {
+ const invalidateNotifications = jest.fn();
+ const removeWaveDeliveredNotifications = jest
+ .fn()
+ .mockResolvedValue(undefined);
+
+ beforeEach(() => {
+ invalidateNotifications.mockClear();
+ removeWaveDeliveredNotifications.mockClear();
+ (
+ commonApiPostWithoutBodyAndResponse as jest.MockedFunction<
+ typeof commonApiPostWithoutBodyAndResponse
+ >
+ ).mockClear();
+ });
+
+ it("skips read-sync when disabled", () => {
+ render(
+
+
+
+ );
+
+ expect(removeWaveDeliveredNotifications).not.toHaveBeenCalled();
+ expect(commonApiPostWithoutBodyAndResponse).not.toHaveBeenCalled();
+ expect(invalidateNotifications).not.toHaveBeenCalled();
+ });
+
+ it("marks the wave as read when enabled", async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(removeWaveDeliveredNotifications).toHaveBeenCalledWith("wave-1");
+ expect(commonApiPostWithoutBodyAndResponse).toHaveBeenCalledWith({
+ endpoint: "notifications/wave/wave-1/read",
+ });
+ expect(invalidateNotifications).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/components/brain/BrainMobile.tsx b/components/brain/BrainMobile.tsx
index fa0a6276f3..360a4d3856 100644
--- a/components/brain/BrainMobile.tsx
+++ b/components/brain/BrainMobile.tsx
@@ -33,6 +33,7 @@ import BrainMobileViewContent from "./mobile/BrainMobileViewContent";
import FloatingMemesQuickVoteTrigger from "./mobile/FloatingMemesQuickVoteTrigger";
import { BrainView } from "./mobile/brainMobileViews";
import { useBrainMobileActiveView } from "./mobile/useBrainMobileActiveView";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
interface Props {
readonly children: ReactNode;
@@ -44,6 +45,7 @@ const BrainMobile: React.FC = ({ children }) => {
const pathname = usePathname();
const { isApp } = useDeviceInfo();
const { connectedProfile } = useAuth();
+ const { isConnected } = useSeizeConnectContext();
const quickVote = useMemesQuickVoteDialogController();
const hydrated = useSyncExternalStore(
() => () => {},
@@ -100,6 +102,7 @@ const BrainMobile: React.FC = ({ children }) => {
firstDecisionDone,
isApp,
isCompleted,
+ isConnected,
isCurationWave,
isMemesWave,
isRankWave,
diff --git a/components/brain/ContentTabContext.tsx b/components/brain/ContentTabContext.tsx
index 6e4b4ce69b..3827a93cc5 100644
--- a/components/brain/ContentTabContext.tsx
+++ b/components/brain/ContentTabContext.tsx
@@ -23,6 +23,7 @@ export enum WaveVotingState {
type WaveTabParams = {
waveId: string | null;
isChatWave: boolean;
+ isConnected: boolean;
isMemesWave: boolean;
isCurationWave: boolean;
votingState: WaveVotingState;
@@ -52,6 +53,7 @@ const isValidWaveTabMap = (
};
const buildMemesTabs = (
+ isConnected: boolean,
votingState: WaveVotingState,
hasFirstDecisionPassed: boolean
) => {
@@ -63,7 +65,9 @@ const buildMemesTabs = (
if (hasFirstDecisionPassed) {
tabs.push(MyStreamWaveTab.WINNERS);
}
- tabs.push(MyStreamWaveTab.MY_VOTES);
+ if (isConnected) {
+ tabs.push(MyStreamWaveTab.MY_VOTES);
+ }
tabs.push(MyStreamWaveTab.OUTCOME);
tabs.push(MyStreamWaveTab.FAQ);
return tabs;
@@ -142,6 +146,7 @@ export const ContentTabProvider: React.FC<{ children: ReactNode }> = ({
const {
waveId,
isChatWave,
+ isConnected,
isMemesWave,
isCurationWave,
votingState,
@@ -152,7 +157,7 @@ export const ContentTabProvider: React.FC<{ children: ReactNode }> = ({
if (isChatWave) {
tabs = [MyStreamWaveTab.CHAT];
} else if (isMemesWave) {
- tabs = buildMemesTabs(votingState, hasFirstDecisionPassed);
+ tabs = buildMemesTabs(isConnected, votingState, hasFirstDecisionPassed);
} else {
tabs = buildDefaultTabs(
votingState,
diff --git a/components/brain/mobile/BrainMobileTabs.tsx b/components/brain/mobile/BrainMobileTabs.tsx
index 96e370662b..785dd8a238 100644
--- a/components/brain/mobile/BrainMobileTabs.tsx
+++ b/components/brain/mobile/BrainMobileTabs.tsx
@@ -11,6 +11,7 @@ import { ArrowLeftIcon } from "@heroicons/react/24/solid";
import { useUnreadIndicator } from "@/hooks/useUnreadIndicator";
import { useUnreadNotifications } from "@/hooks/useUnreadNotifications";
import { useAuth } from "@/components/auth/Auth";
+import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { getWaveHomeRoute } from "../../../helpers/navigation.helpers";
interface BrainMobileTabsProps {
@@ -35,6 +36,7 @@ const BrainMobileTabs: React.FC = ({
const router = useRouter();
const { registerRef } = useLayout();
const { connectedProfile } = useAuth();
+ const { isConnected } = useSeizeConnectContext();
// Local ref for component-specific needs
const mobileTabsRef = useRef(null);
@@ -264,7 +266,7 @@ const BrainMobileTabs: React.FC = ({
}}
renderAfterLeaderboard={salesTabButton}
/>
- {(isMemesWave || isCurationWave) && (
+ {(isCurationWave || (isMemesWave && isConnected)) && (
<>