Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a9cbc30
wip
simo6529 Apr 30, 2026
5ffc0b3
wip
simo6529 May 1, 2026
5e849cd
wip
simo6529 May 1, 2026
0e0f5d9
wip
simo6529 May 1, 2026
3610289
wip
simo6529 May 1, 2026
91b2665
wip
simo6529 May 1, 2026
9a9a050
wip
simo6529 May 1, 2026
6e4df0e
wip
simo6529 May 2, 2026
6182b57
wip
simo6529 May 2, 2026
c0cfd64
wip
simo6529 May 2, 2026
a78754d
wip
simo6529 May 2, 2026
513d855
wip
simo6529 May 2, 2026
ebbe47f
wip
simo6529 May 2, 2026
b898515
wip
simo6529 May 2, 2026
67f564e
wip
simo6529 May 2, 2026
84a3457
wip
simo6529 May 2, 2026
b27f1dd
wip
simo6529 May 2, 2026
37cb73e
wip
simo6529 May 2, 2026
dc9c9e2
wip
simo6529 May 3, 2026
0a98120
wip
simo6529 May 4, 2026
65ef2f6
wip
simo6529 May 4, 2026
6ee47cd
wip
simo6529 May 4, 2026
ae363c5
wip
simo6529 May 4, 2026
0d9ebca
Merge branch 'main' into notifications-n-plus-one
simo6529 May 4, 2026
f362f01
wip
simo6529 May 4, 2026
8451d7f
wip
simo6529 May 4, 2026
0378392
wip
simo6529 May 5, 2026
9a4b04f
wip
simo6529 May 5, 2026
fe9b104
wip
simo6529 May 5, 2026
49b1d7a
Merge branch 'main' into notifications-n-plus-one
simo6529 May 5, 2026
cfdbf01
wip
simo6529 May 5, 2026
37f485e
wip
simo6529 May 5, 2026
03cc64c
wip
simo6529 May 5, 2026
936e77c
wip
simo6529 May 5, 2026
d0fa796
wip
simo6529 May 5, 2026
2fd9d25
wip
simo6529 May 6, 2026
7709c7d
wip
simo6529 May 6, 2026
2747198
wip
simo6529 May 6, 2026
f5756e0
wip
simo6529 May 6, 2026
24a2ac3
wip
simo6529 May 6, 2026
c1babb9
wip
simo6529 May 6, 2026
63f3fb8
wip
simo6529 May 6, 2026
1b73142
wip
simo6529 May 6, 2026
7d65e27
Merge branch 'main' into notifications-n-plus-one
simo6529 May 7, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ jest.mock("@/hooks/useConnectedAccountsUnreadNotifications", () => ({
useConnectedAccountsUnreadNotifications: () => ({}),
}));

jest.mock("@/hooks/useUnreadNotifications", () => ({
useUnreadNotifications: () => ({
notifications: { unread_count: 0 },
haveUnreadNotifications: false,
}),
}));

jest.mock("@/services/auth/auth.utils", () => ({
WALLET_ACCOUNTS_UPDATED_EVENT: "6529-wallet-accounts-updated",
canStoreAnotherWalletAccount: jest.fn(() => true),
Expand Down
155 changes: 152 additions & 3 deletions __tests__/components/auth/SeizeConnectContext.switch-sync.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ jest.mock("@/hooks/useConnectedAccountsUnreadNotifications", () => ({
useConnectedAccountsUnreadNotifications: jest.fn(() => ({})),
}));

jest.mock("@/hooks/useUnreadNotifications", () => ({
useUnreadNotifications: jest.fn(() => ({
notifications: { unread_count: 0 },
haveUnreadNotifications: false,
})),
}));

jest.mock("@/services/auth/auth.utils", () => ({
canStoreAnotherWalletAccount: jest.fn(() => true),
getConnectedWalletAccounts: jest.fn(() => []),
Expand All @@ -54,20 +61,40 @@ const AddressProbe: React.FC = () => {
return <div data-testid="active-address">{address ?? "undefined"}</div>;
};

const UnreadProbe: React.FC = () => {
const { connectedAccountUnreadNotifications } = useSeizeConnectContext();
return (
<div data-testid="unread-map">
{JSON.stringify(connectedAccountUnreadNotifications)}
</div>
);
};

const buildStoredAccount = (
address: string
address: string,
profileHandle: string | null = null,
jwt: string | null = null
): authUtils.ConnectedWalletAccount => ({
address,
refreshToken: "dummy-refresh-token",
role: null,
jwt: null,
jwt,
profileId: null,
profileHandle: null,
profileHandle,
});

describe("SeizeConnectContext switch sync guard", () => {
beforeEach(() => {
jest.clearAllMocks();
require("@/hooks/useConnectedAccountsUnreadNotifications").useConnectedAccountsUnreadNotifications.mockReturnValue(
{}
);
require("@/hooks/useUnreadNotifications").useUnreadNotifications.mockReturnValue(
{
notifications: { unread_count: 0 },
haveUnreadNotifications: false,
}
);
});

it("prefers stored active account while provider still reports previous known account", async () => {
Expand Down Expand Up @@ -185,4 +212,126 @@ describe("SeizeConnectContext switch sync guard", () => {
expect(screen.getByTestId("active-address")).toHaveTextContent(addressA);
});
});

it("polls inactive accounts only and merges the active unread count", async () => {
const { useAppKitAccount } = require("@reown/appkit/react");
const mockGetWalletAddress =
authUtils.getWalletAddress as jest.MockedFunction<
typeof authUtils.getWalletAddress
>;
const mockGetConnectedWalletAccounts =
authUtils.getConnectedWalletAccounts as jest.MockedFunction<
typeof authUtils.getConnectedWalletAccounts
>;
const mockUseConnectedAccountsUnreadNotifications =
require("@/hooks/useConnectedAccountsUnreadNotifications")
.useConnectedAccountsUnreadNotifications as jest.Mock;
const mockUseUnreadNotifications = require("@/hooks/useUnreadNotifications")
.useUnreadNotifications as jest.Mock;

const activeAccount = buildStoredAccount(addressA, "alice");
const inactiveAccount = buildStoredAccount(addressB, "bob");

(useAppKitAccount as jest.Mock).mockReturnValue({
address: addressA,
isConnected: true,
status: "connected",
});
mockGetWalletAddress.mockReturnValue(addressA);
mockGetConnectedWalletAccounts.mockReturnValue([
activeAccount,
inactiveAccount,
]);
mockUseConnectedAccountsUnreadNotifications.mockReturnValue({
[addressB]: 4,
});
mockUseUnreadNotifications.mockReturnValue({
notifications: { unread_count: 7 },
haveUnreadNotifications: true,
});

render(
<SeizeConnectProvider>
<UnreadProbe />
</SeizeConnectProvider>
);

await waitFor(() => {
expect(mockUseConnectedAccountsUnreadNotifications).toHaveBeenCalledWith([
inactiveAccount,
]);
expect(mockUseUnreadNotifications).toHaveBeenCalledWith("alice");
});

const unreadMap = JSON.parse(
screen.getByTestId("unread-map").textContent ?? "{}"
);

expect(unreadMap).toEqual({
[addressA]: 7,
[addressB]: 4,
});
});

it("keeps the active JWT unread count when the active account has no profile handle", async () => {
const { useAppKitAccount } = require("@reown/appkit/react");
const mockGetWalletAddress =
authUtils.getWalletAddress as jest.MockedFunction<
typeof authUtils.getWalletAddress
>;
const mockGetConnectedWalletAccounts =
authUtils.getConnectedWalletAccounts as jest.MockedFunction<
typeof authUtils.getConnectedWalletAccounts
>;
const mockUseConnectedAccountsUnreadNotifications =
require("@/hooks/useConnectedAccountsUnreadNotifications")
.useConnectedAccountsUnreadNotifications as jest.Mock;
const mockUseUnreadNotifications = require("@/hooks/useUnreadNotifications")
.useUnreadNotifications as jest.Mock;

const activeAccount = buildStoredAccount(addressA, null, "active-jwt");
const inactiveAccount = buildStoredAccount(addressB, "bob", "inactive-jwt");

(useAppKitAccount as jest.Mock).mockReturnValue({
address: addressA,
isConnected: true,
status: "connected",
});
mockGetWalletAddress.mockReturnValue(addressA);
mockGetConnectedWalletAccounts.mockReturnValue([
activeAccount,
inactiveAccount,
]);
mockUseConnectedAccountsUnreadNotifications.mockReturnValue({
[addressA]: 9,
[addressB]: 4,
});
mockUseUnreadNotifications.mockReturnValue({
notifications: { unread_count: 0 },
haveUnreadNotifications: false,
});

render(
<SeizeConnectProvider>
<UnreadProbe />
</SeizeConnectProvider>
);

await waitFor(() => {
expect(mockUseConnectedAccountsUnreadNotifications).toHaveBeenCalledWith([
activeAccount,
inactiveAccount,
]);
expect(mockUseUnreadNotifications).toHaveBeenCalledWith(null);
});

const unreadMap = JSON.parse(
screen.getByTestId("unread-map").textContent ?? "{}"
);

expect(unreadMap).toEqual({
[addressA]: 9,
[addressB]: 4,
});
});
});
11 changes: 11 additions & 0 deletions __tests__/components/auth/SeizeConnectContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ jest.mock("viem", () => ({
getAddress: jest.fn((address: string) => address.toLowerCase()),
}));

jest.mock("@/hooks/useConnectedAccountsUnreadNotifications", () => ({
useConnectedAccountsUnreadNotifications: jest.fn(() => ({})),
}));

jest.mock("@/hooks/useUnreadNotifications", () => ({
useUnreadNotifications: jest.fn(() => ({
notifications: { unread_count: 0 },
haveUnreadNotifications: false,
})),
}));

// Mock auth utils
jest.mock("@/services/auth/auth.utils", () => ({
canStoreAnotherWalletAccount: jest.fn(() => true),
Expand Down
49 changes: 39 additions & 10 deletions __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ const invalidateNotificationsMock = jest.fn();
const mockUseAuth = jest.fn();
const mockApprovalStatus = jest.fn();

let documentVisibilityState: DocumentVisibilityState = "visible";

const setDocumentVisibilityState = (state: DocumentVisibilityState) => {
documentVisibilityState = state;
Object.defineProperty(document, "visibilityState", {
configurable: true,
get: () => documentVisibilityState,
});
};

jest.mock("next/navigation", () => ({
useRouter: () => ({ replace: replaceMock }),
useSearchParams: () => searchParamsMock,
Expand Down Expand Up @@ -115,29 +125,41 @@ jest.mock("@/components/auth/Auth", () => ({
useAuth: () => mockUseAuth(),
}));

jest.mock("@/components/auth/SeizeConnectContext", () => ({
useSeizeConnectContext: () => ({ address: "0xAAA" }),
}));

jest.mock("@/services/api/common-api", () => ({
commonApiPostWithoutBodyAndResponse: jest.fn().mockResolvedValue(undefined),
}));

jest.mock("@/services/auth/auth.utils", () => ({
getAuthJwt: () => "test-jwt",
}));

jest.mock("jwt-decode", () => ({
jwtDecode: (token: string) => {
if (token !== "test-jwt") {
throw new Error(`Unexpected JWT decode for ${token}`);
}

return { sub: "0xAAA", role: null, exp: 4102444800 };
},
}));

const wave = {
id: "10",
participation: {},
metrics: { muted: false, your_unread_drops_count: 0 },
wave: { type: ApiWaveType.Rank, winning_threshold: null },
} as any;
const mockOnDropClick = jest.fn();
const setDocumentVisibility = (visibilityState: DocumentVisibilityState) => {
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: visibilityState,
});
};

describe("MyStreamWaveChat", () => {
let store: any;

beforeEach(() => {
setDocumentVisibility("visible");
setDocumentVisibilityState("visible");
capturedPropsHolder.current = {};
capturedCreatorPropsHolder.current = {};
capturedMemesButtonPropsHolder.current = {};
Expand All @@ -160,6 +182,7 @@ describe("MyStreamWaveChat", () => {
});
mockUseAuth.mockReturnValue({
connectedProfile: { handle: "tester" },
activeProfileProxy: null,
});
(
commonApiPostWithoutBodyAndResponse as jest.MockedFunction<
Expand Down Expand Up @@ -319,13 +342,16 @@ describe("MyStreamWaveChat", () => {
expect(mockRemoveWaveDeliveredNotifications).toHaveBeenCalledWith("10");
expect(commonApiPostWithoutBodyAndResponse).toHaveBeenCalledWith({
endpoint: "notifications/wave/10/read",
headers: { Authorization: "Bearer test-jwt" },
});
expect(invalidateNotificationsMock).toHaveBeenCalled();
});
});

it("does not call the read endpoint on unmount when the tab is hidden", async () => {
setDocumentVisibility("hidden");
it("does not mark notifications read on hidden unmount", async () => {
setDocumentVisibilityState("hidden");
searchParamsMock.get.mockReturnValueOnce("5").mockReturnValue(null);
searchParamsMock.toString.mockReturnValue("serialNo=5");

const { unmount } = renderWithProvider(
<MyStreamWaveChat
Expand All @@ -338,16 +364,19 @@ describe("MyStreamWaveChat", () => {

await act(async () => {
unmount();
await Promise.resolve();
});

expect(mockSetUnreadDividerSerialNo).toHaveBeenCalledWith(null);
expect(mockRemoveWaveDeliveredNotifications).not.toHaveBeenCalled();
expect(commonApiPostWithoutBodyAndResponse).not.toHaveBeenCalled();
expect(invalidateNotificationsMock).not.toHaveBeenCalled();
expect(mockRemoveWaveDeliveredNotifications).not.toHaveBeenCalled();
});

it("skips notification cleanup on unmount for anonymous viewers", async () => {
mockUseAuth.mockReturnValue({
connectedProfile: null,
activeProfileProxy: null,
});

const { unmount } = renderWithProvider(
Expand Down
Loading
Loading