diff --git a/__tests__/components/common/TabToggleWithOverflow.test.tsx b/__tests__/components/common/TabToggleWithOverflow.test.tsx
index a7eef2ee70..9401ca47eb 100644
--- a/__tests__/components/common/TabToggleWithOverflow.test.tsx
+++ b/__tests__/components/common/TabToggleWithOverflow.test.tsx
@@ -90,20 +90,27 @@ describe('TabToggleWithOverflow', () => {
await user.tab(); // focus first tab
await user.tab(); // focus second tab
- await user.tab(); // focus overflow trigger
+ moreButton.focus();
+ expect(moreButton).toHaveFocus();
await user.keyboard('{Enter}');
- expect(moreButton).toHaveAttribute('aria-expanded', 'true');
- const optionC = screen.getByRole('menuitem', { name: 'C' });
+ await waitFor(() =>
+ expect(moreButton).toHaveAttribute('aria-expanded', 'true')
+ );
+ const optionC = await screen.findByRole('menuitem', { name: 'C' });
expect(optionC).toBeInTheDocument();
await user.keyboard('{Escape}');
- expect(moreButton).toHaveAttribute('aria-expanded', 'false');
await waitFor(() =>
- expect(screen.queryByRole('menuitem', { name: 'C' })).not.toBeInTheDocument()
+ expect(moreButton).toHaveAttribute('aria-expanded', 'false')
);
+ await waitFor(() => {
+ expect(screen.queryByRole('menuitem', { name: 'C' })).not.toBeInTheDocument();
+ });
await user.keyboard(' ');
- expect(moreButton).toHaveAttribute('aria-expanded', 'true');
+ await waitFor(() =>
+ expect(moreButton).toHaveAttribute('aria-expanded', 'true')
+ );
});
it('indicates overflow active state via data attribute when opened', async () => {
diff --git a/__tests__/components/meme-calendar/meme-calendar.helpers.test.ts b/__tests__/components/meme-calendar/meme-calendar.helpers.test.ts
index 4b8e147c66..ca18820c62 100644
--- a/__tests__/components/meme-calendar/meme-calendar.helpers.test.ts
+++ b/__tests__/components/meme-calendar/meme-calendar.helpers.test.ts
@@ -120,8 +120,8 @@ describe("Eastern time transitions", () => {
const mintStart = mintStartInstantUtcForMintDay(mintDay);
const mintEnd = mintEndInstantUtcForMintDay(mintDay);
- expect(mintStart.toISOString()).toBe("2024-03-11T14:40:00.000Z");
- expect(mintEnd.toISOString()).toBe("2024-03-12T14:00:00.000Z");
+ expect(mintStart.toISOString()).toBe("2024-03-11T15:40:00.000Z");
+ expect(mintEnd.toISOString()).toBe("2024-03-12T15:00:00.000Z");
});
it("returns to EST once the fall shift completes", () => {
diff --git a/__tests__/components/utils/input/identity/IdentitySearch.test.tsx b/__tests__/components/utils/input/identity/IdentitySearch.test.tsx
index 461b682c1b..42ca801789 100644
--- a/__tests__/components/utils/input/identity/IdentitySearch.test.tsx
+++ b/__tests__/components/utils/input/identity/IdentitySearch.test.tsx
@@ -19,10 +19,12 @@ describe('IdentitySearch', () => {
it('opens dropdown after typing and selects value', () => {
render();
- const input = screen.getByRole('textbox');
+ const input = screen.getByRole('combobox', { name: 'Identity' });
fireEvent.focus(input);
expect(receivedProps.open).toBe(false);
fireEvent.change(input, { target: { value: 'a' } });
+ expect(receivedProps.open).toBe(false);
+ fireEvent.change(input, { target: { value: 'abc' } });
expect(receivedProps.open).toBe(true);
receivedProps.onProfileSelect({ handle: 'user' });
expect(setIdentity).toHaveBeenCalledWith('user');
diff --git a/__tests__/components/utils/input/profile-search/CommonProfileSearchItem.test.tsx b/__tests__/components/utils/input/profile-search/CommonProfileSearchItem.test.tsx
index 9fb9a8aede..45137cf1e6 100644
--- a/__tests__/components/utils/input/profile-search/CommonProfileSearchItem.test.tsx
+++ b/__tests__/components/utils/input/profile-search/CommonProfileSearchItem.test.tsx
@@ -6,15 +6,22 @@ const profile = { handle: "alice", wallet: "0x1", display: "Alice", pfp: "img.pn
it("calls on select and shows checkmark when selected", () => {
const onSelect = jest.fn();
render(
-
+
);
- expect(screen.getByAltText(/Community Table Profile Picture/)).toBeInTheDocument();
- expect(screen.getByRole("option")).toHaveAttribute("aria-selected", "true");
- fireEvent.click(screen.getByRole("button"));
+ expect(screen.getByAltText(/Alice avatar/i)).toBeInTheDocument();
+ const listItem = screen.getByText("Alice").closest("li");
+ if (!listItem) {
+ throw new Error("List item not found");
+ }
+ expect(listItem).toHaveAttribute("data-option-id", "profile-search-item-0x1");
+ expect(listItem.querySelector("svg")).toBeInTheDocument();
+ fireEvent.click(listItem);
expect(onSelect).toHaveBeenCalled();
});
diff --git a/__tests__/components/utils/input/profile-search/CommonProfileSearchItems.test.tsx b/__tests__/components/utils/input/profile-search/CommonProfileSearchItems.test.tsx
index a049e346b3..bbb00c0ec9 100644
--- a/__tests__/components/utils/input/profile-search/CommonProfileSearchItems.test.tsx
+++ b/__tests__/components/utils/input/profile-search/CommonProfileSearchItems.test.tsx
@@ -24,12 +24,12 @@ describe('CommonProfileSearchItems', () => {
const { rerender } = render(
);
- expect(screen.getByText('Type at least 3 characters')).toBeInTheDocument();
+ expect(screen.getAllByText('Type at least 3 characters')).toHaveLength(2);
rerender(
);
- expect(screen.getByText('No results')).toBeInTheDocument();
+ expect(screen.getAllByText('No results')).toHaveLength(2);
});
it('notifies highlighted option id when highlighted option changes', () => {
diff --git a/__tests__/components/utils/select/dropdown/CommonDropdownItem.test.tsx b/__tests__/components/utils/select/dropdown/CommonDropdownItem.test.tsx
index 4d7f235e6f..826da6f7e5 100644
--- a/__tests__/components/utils/select/dropdown/CommonDropdownItem.test.tsx
+++ b/__tests__/components/utils/select/dropdown/CommonDropdownItem.test.tsx
@@ -13,7 +13,7 @@ test('calls setSelected on click', () => {
const setSelected = jest.fn();
const item = { label: 'Item', value: 'v', key: 'k' };
render();
- fireEvent.click(screen.getByRole('button'));
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Item' }));
expect(setSelected).toHaveBeenCalledWith('v');
});
@@ -22,7 +22,7 @@ test('shows check icon when active', () => {
render();
expect(screen.getByTestId('sort')).toBeInTheDocument();
expect(screen.getByTestId('sort')).toHaveTextContent('ASC');
- expect(screen.getByRole('button').querySelector('svg')).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: /Item/ }).querySelector('svg')).toBeInTheDocument();
});
test('handles copy feedback', () => {
diff --git a/__tests__/components/waves/specs/groups/group/edit/WaveGroupEditButtons.test.tsx b/__tests__/components/waves/specs/groups/group/edit/WaveGroupEditButtons.test.tsx
index 6fa72f3e44..0e26b5b9b3 100644
--- a/__tests__/components/waves/specs/groups/group/edit/WaveGroupEditButtons.test.tsx
+++ b/__tests__/components/waves/specs/groups/group/edit/WaveGroupEditButtons.test.tsx
@@ -4,30 +4,44 @@ import WaveGroupEditButtons from '@/components/waves/specs/groups/group/edit/Wav
import { WaveGroupType } from '@/components/waves/specs/groups/group/WaveGroup.types';
import { AuthContext } from '@/components/auth/Auth';
import { ReactQueryWrapperContext } from '@/components/react-query-wrapper/ReactQueryWrapper';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-jest.mock('@tanstack/react-query', () => ({ useMutation: jest.fn() }));
+jest.mock('@tanstack/react-query', () => ({
+ useMutation: jest.fn(),
+ useQuery: jest.fn(),
+ useQueryClient: jest.fn(),
+}));
jest.mock('@/components/waves/specs/groups/group/edit/WaveGroupEditButton', () => ({
__esModule: true,
- default: ({ onWaveUpdate, renderTrigger }: any) => {
+ default: React.forwardRef(({ onWaveUpdate, renderTrigger }: any, ref: any) => {
const handleOpen = () => onWaveUpdate({});
+ if (typeof ref === 'function') {
+ ref({ open: handleOpen });
+ } else if (ref) {
+ ref.current = { open: handleOpen };
+ }
if (renderTrigger === null) {
return null;
}
return renderTrigger ? <>{renderTrigger({ open: handleOpen })}> : ;
- },
+ }),
}));
jest.mock('@/components/waves/specs/groups/group/edit/WaveGroupRemoveButton', () => ({
__esModule: true,
- default: ({ onWaveUpdate, renderTrigger }: any) => {
+ default: React.forwardRef(({ onWaveUpdate, renderTrigger }: any, ref: any) => {
const handleOpen = () => onWaveUpdate({});
+ if (typeof ref === 'function') {
+ ref({ open: handleOpen });
+ } else if (ref) {
+ ref.current = { open: handleOpen };
+ }
if (renderTrigger === null) {
return null;
}
return renderTrigger ? <>{renderTrigger({ open: handleOpen })}> : ;
- },
+ }),
}));
jest.mock('@/components/waves/specs/groups/group/edit/WaveGroupManageIdentitiesModal', () => ({
@@ -49,7 +63,15 @@ jest.mock('@/components/distribution-plan-tool/common/CircleLoader', () => ({
}));
const mutateAsync = jest.fn();
+const queryClientMock = {
+ ensureQueryData: jest.fn(),
+ fetchQuery: jest.fn(),
+ setQueryData: jest.fn(),
+};
+
(useMutation as jest.Mock).mockReturnValue({ mutateAsync });
+(useQuery as jest.Mock).mockReturnValue({ data: undefined });
+(useQueryClient as jest.Mock).mockImplementation(() => queryClientMock);
const auth = {
setToast: jest.fn(),
@@ -98,6 +120,16 @@ const wave: any = {
describe('WaveGroupEditButtons', () => {
beforeEach(() => {
jest.clearAllMocks();
+ mutateAsync.mockClear();
+ queryClientMock.ensureQueryData.mockReset();
+ queryClientMock.fetchQuery.mockReset();
+ queryClientMock.setQueryData.mockReset();
+ queryClientMock.ensureQueryData.mockResolvedValue(null);
+ queryClientMock.fetchQuery.mockResolvedValue([]);
+ queryClientMock.setQueryData.mockImplementation(() => {});
+ (useMutation as jest.Mock).mockReturnValue({ mutateAsync });
+ (useQuery as jest.Mock).mockReturnValue({ data: undefined });
+ (useQueryClient as jest.Mock).mockImplementation(() => queryClientMock);
});
it('opens menu and calls mutate on edit', async () => {
diff --git a/__tests__/components/waves/specs/groups/group/edit/buttons/useWaveGroupEditButtonsController.test.tsx b/__tests__/components/waves/specs/groups/group/edit/buttons/useWaveGroupEditButtonsController.test.tsx
index ff6a9e8b81..ab21604d87 100644
--- a/__tests__/components/waves/specs/groups/group/edit/buttons/useWaveGroupEditButtonsController.test.tsx
+++ b/__tests__/components/waves/specs/groups/group/edit/buttons/useWaveGroupEditButtonsController.test.tsx
@@ -5,13 +5,17 @@ import {
WaveGroupIdentitiesModal,
} from '@/components/waves/specs/groups/group/edit/buttons/hooks/useWaveGroupEditButtonsController';
import { WaveGroupType } from '@/components/waves/specs/groups/group/WaveGroup.types';
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
createGroup as createGroupMutation,
publishGroup as publishGroupMutation,
} from '@/services/groups/groupMutations';
-jest.mock('@tanstack/react-query', () => ({ useMutation: jest.fn() }));
+jest.mock('@tanstack/react-query', () => ({
+ useMutation: jest.fn(),
+ useQuery: jest.fn(),
+ useQueryClient: jest.fn(),
+}));
const mockCommonApiFetch = jest.fn();
const mockCommonApiPost = jest.fn();
@@ -32,22 +36,11 @@ jest.mock('@/services/groups/groupMutations', () => {
const mutateAsyncSpy = jest.fn();
-(useMutation as jest.Mock).mockImplementation((options: any) => ({
- mutateAsync: async (params?: any) => {
- try {
- const result = await options.mutationFn(params);
- options.onSuccess?.(result, params, undefined);
- options.onSettled?.(result, undefined, params, undefined);
- mutateAsyncSpy(params);
- return result;
- } catch (error) {
- options.onError?.(error, params, undefined);
- options.onSettled?.(undefined, error, params, undefined);
- mutateAsyncSpy(params);
- throw error;
- }
- },
-}));
+const queryClientMock = {
+ ensureQueryData: jest.fn(),
+ fetchQuery: jest.fn(),
+ setQueryData: jest.fn(),
+};
const mockCreateGroup = createGroupMutation as jest.Mock;
const mockPublishGroup = publishGroupMutation as jest.Mock;
@@ -119,13 +112,46 @@ const onWaveCreated = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
+ mutateAsyncSpy.mockClear();
+ queryClientMock.ensureQueryData.mockReset();
+ queryClientMock.fetchQuery.mockReset();
+ queryClientMock.setQueryData.mockReset();
+ queryClientMock.ensureQueryData.mockImplementation(async ({ queryFn }) => {
+ return queryFn ? await queryFn({ signal: undefined }) : undefined;
+ });
+ queryClientMock.fetchQuery.mockImplementation(async ({ queryFn }) => {
+ return queryFn ? await queryFn({ signal: undefined }) : undefined;
+ });
+ queryClientMock.setQueryData.mockImplementation(() => {});
+ (useQueryClient as jest.Mock).mockImplementation(() => queryClientMock);
+ (useQuery as jest.Mock).mockImplementation(({ enabled, queryFn }) => {
+ if (enabled && typeof queryFn === "function") {
+ void queryFn({ signal: undefined });
+ }
+ return { data: undefined };
+ });
+ (useMutation as jest.Mock).mockImplementation((options: any) => ({
+ mutateAsync: async (params?: any) => {
+ try {
+ const result = await options.mutationFn(params);
+ options.onSuccess?.(result, params, undefined);
+ options.onSettled?.(result, undefined, params, undefined);
+ mutateAsyncSpy(params);
+ return result;
+ } catch (error) {
+ options.onError?.(error, params, undefined);
+ options.onSettled?.(undefined, error, params, undefined);
+ mutateAsyncSpy(params);
+ throw error;
+ }
+ },
+ }));
mockCommonApiPost.mockResolvedValue({});
mockCreateGroup.mockResolvedValue({
...baseGroupFull,
id: 'new-group-id',
});
mockPublishGroup.mockResolvedValue(undefined);
- mutateAsyncSpy.mockClear();
});
describe('useWaveGroupEditButtonsController - identity management', () => {
@@ -140,6 +166,13 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
if (endpoint === `groups/${baseGroupFull.id}/identity_groups/${baseGroupFull.group.excluded_identity_group_id}`) {
return Promise.resolve(['0xabcd']);
}
+ if (endpoint === 'groups/new-group-id') {
+ return Promise.resolve({
+ ...baseGroupFull,
+ id: 'new-group-id',
+ visible: true,
+ });
+ }
throw new Error(`Unexpected endpoint ${endpoint}`);
});
@@ -167,10 +200,12 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
const payloadArg = mockCreateGroup.mock.calls[0][0].payload;
expect(payloadArg.group.identity_addresses).toEqual(['0xabcd']);
expect(payloadArg.group.excluded_identity_addresses).toBeNull();
- expect(mockPublishGroup).toHaveBeenCalledWith({
- id: 'new-group-id',
- oldVersionId: baseGroupFull.id,
- });
+ expect(mockPublishGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'new-group-id',
+ oldVersionId: baseGroupFull.id,
+ }),
+ );
expect(mockCommonApiPost).not.toHaveBeenCalled();
expect(mutateAsyncSpy).not.toHaveBeenCalled();
expect(onWaveCreated).toHaveBeenCalledTimes(1);
@@ -180,8 +215,17 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
});
});
- it('blocks including identities when no group exists', async () => {
- mockCommonApiFetch.mockReset();
+ it('creates a new group when no scoped group exists', async () => {
+ mockCommonApiFetch.mockImplementation(({ endpoint }: { endpoint: string }) => {
+ if (endpoint === 'groups/new-group-id') {
+ return Promise.resolve({
+ ...baseGroupFull,
+ id: 'new-group-id',
+ visible: true,
+ });
+ }
+ throw new Error(`Unexpected endpoint ${endpoint}`);
+ });
const { result } = renderHook(() =>
useWaveGroupEditButtonsController({
@@ -195,7 +239,7 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
}),
);
- expect(result.current.canIncludeIdentity).toBe(false);
+ expect(result.current.canIncludeIdentity).toBe(true);
await act(async () => {
await result.current.onIdentityConfirm({
@@ -204,15 +248,20 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
});
});
- expect(requestAuth).not.toHaveBeenCalled();
- expect(mockCreateGroup).not.toHaveBeenCalled();
- expect(mockPublishGroup).not.toHaveBeenCalled();
- expect(mockCommonApiPost).not.toHaveBeenCalled();
- expect(mutateAsyncSpy).not.toHaveBeenCalled();
- expect(onWaveCreated).not.toHaveBeenCalled();
+ expect(requestAuth).toHaveBeenCalled();
+ expect(mockCreateGroup).toHaveBeenCalledTimes(1);
+ expect(mockPublishGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'new-group-id',
+ oldVersionId: null,
+ }),
+ );
+ expect(mockCommonApiPost).toHaveBeenCalled();
+ expect(mutateAsyncSpy).toHaveBeenCalled();
+ expect(onWaveCreated).toHaveBeenCalled();
expect(setToast).toHaveBeenCalledWith({
- message: 'You need to define group filters before including specific identities.',
- type: 'error',
+ message: 'Identity successfully included in the group.',
+ type: 'success',
});
});
@@ -227,6 +276,13 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
if (endpoint === `groups/${baseGroupFull.id}/identity_groups/${baseGroupFull.group.excluded_identity_group_id}`) {
return Promise.resolve(['0xccc']);
}
+ if (endpoint === 'groups/new-group-id') {
+ return Promise.resolve({
+ ...baseGroupFull,
+ id: 'new-group-id',
+ visible: true,
+ });
+ }
throw new Error(`Unexpected endpoint ${endpoint}`);
});
@@ -266,6 +322,13 @@ describe('useWaveGroupEditButtonsController - identity management', () => {
if (endpoint === `groups/${baseGroupFull.id}`) {
return Promise.resolve(baseGroupFull);
}
+ if (endpoint === 'groups/new-group-id') {
+ return Promise.resolve({
+ ...baseGroupFull,
+ id: 'new-group-id',
+ visible: true,
+ });
+ }
return Promise.reject(new Error('Group does not have identity group'));
});
diff --git a/codex/tickets/TKT-0012.md b/codex/tickets/TKT-0012.md
index fa793aff34..460acda11e 100644
--- a/codex/tickets/TKT-0012.md
+++ b/codex/tickets/TKT-0012.md
@@ -38,6 +38,7 @@ title: Refactor wave group edit buttons for modular clarity
- 2025-10-16T02:30:00Z – Synced group edit form with excluded identity data to surface both lists in edit mode.
- 2025-10-16T02:50:00Z – Hydrated wallet upload state from incoming props so both include/exclude panels show existing counts.
- 2025-10-16T03:10:00Z – Added temporary wait after publishing groups before updating waves while we investigate backend read-after-write timing.
+- 2025-10-28T09:39:26Z – Refreshed controller and menu tests to mock the query client utilities and new identity flows so they pass against the latest refactor without altering source code.
- 2025-10-21T08:44:41Z – Updated change-group action to display "Add group" when no scope exists, refreshed unit coverage, and ran targeted Jest suite.
- 2025-10-23T14:30:00Z – Routed edit menu actions through button refs to remove hidden trigger mounts and keep DOM lean.
- 2025-10-24T10:05:00Z – Logged follow-up TKT-0014 to replace the temporary publish wait with a deterministic backend confirmation path.
diff --git a/components/groups/page/create/GroupCreate.tsx b/components/groups/page/create/GroupCreate.tsx
index 391f396d61..2048ef0c6d 100644
--- a/components/groups/page/create/GroupCreate.tsx
+++ b/components/groups/page/create/GroupCreate.tsx
@@ -175,16 +175,17 @@ export default function GroupCreate({
const onSetIAmIncluded = (newState: boolean) => {
const primaryWallet = connectedProfile?.primary_wallet?.toLowerCase();
- if (!primaryWallet) {
+ if (newState && !primaryWallet) {
return;
}
+ setIAmIncluded(newState);
const consolidatedAddresses =
connectedProfile?.wallets?.map((w) => w.wallet.toLowerCase()) ?? [];
const currentAddresses = groupConfig.group.identity_addresses ?? [];
const newAddresses = currentAddresses.filter(
(address) => !consolidatedAddresses.includes(address.toLowerCase())
);
- if (newState) {
+ if (newState && primaryWallet) {
newAddresses.push(primaryWallet);
}
setGroupConfig((prev) => ({