Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
b2c4f3b
wip
simo6529 Oct 14, 2025
67cc8be
wip
simo6529 Oct 15, 2025
241a3bf
wip
simo6529 Oct 15, 2025
d3b334b
wip
simo6529 Oct 15, 2025
574e5d2
wip
simo6529 Oct 15, 2025
a1a9cb0
wip
simo6529 Oct 15, 2025
4278592
wip
simo6529 Oct 15, 2025
bb65a7d
wip
simo6529 Oct 15, 2025
0ce7c0f
wip
simo6529 Oct 16, 2025
10187c2
wip
simo6529 Oct 16, 2025
d94f4ff
wip
simo6529 Oct 16, 2025
031b250
wip
simo6529 Oct 16, 2025
a37490c
wip
simo6529 Oct 16, 2025
93d3d84
wip
simo6529 Oct 16, 2025
b95cb1d
wip
simo6529 Oct 17, 2025
4acfcb6
wip
simo6529 Oct 17, 2025
af9af8a
wip
simo6529 Oct 20, 2025
fcb9f97
wip
simo6529 Oct 21, 2025
40ea0be
wip
simo6529 Oct 21, 2025
78bce5e
wip
simo6529 Oct 21, 2025
406d30c
wip
simo6529 Oct 21, 2025
6e6cfe1
wip
simo6529 Oct 21, 2025
0fdbc12
wip
simo6529 Oct 21, 2025
7fadaee
wip
simo6529 Oct 21, 2025
6f88cb9
wip
simo6529 Oct 21, 2025
63a4749
Merge branch 'main' into block-add-identity-to-wave
simo6529 Oct 22, 2025
63d6ee4
wip
simo6529 Oct 22, 2025
4d671f4
wip
simo6529 Oct 22, 2025
f3e2923
wip
simo6529 Oct 23, 2025
9a87a5e
Merge branch 'main' into block-add-identity-to-wave
simo6529 Oct 23, 2025
ae6d7c5
Merge branch 'main' into block-add-identity-to-wave
simo6529 Oct 23, 2025
6d496e8
wip
simo6529 Oct 23, 2025
d11c137
wip
simo6529 Oct 23, 2025
60ced75
wip
simo6529 Oct 24, 2025
7cc19be
wip
simo6529 Oct 24, 2025
7b6fcd6
Merge branch 'main' into block-add-identity-to-wave
simo6529 Oct 24, 2025
745f24c
wip
simo6529 Oct 24, 2025
e98aaa6
wip
simo6529 Oct 24, 2025
6da6a50
wip
simo6529 Oct 24, 2025
78e293f
wip
simo6529 Oct 24, 2025
6b078ff
wip
simo6529 Oct 24, 2025
fca437d
wip
simo6529 Oct 24, 2025
973b9f5
wip
simo6529 Oct 24, 2025
dc0c028
wip
simo6529 Oct 24, 2025
c559908
wip
simo6529 Oct 24, 2025
08dd3a6
wip
simo6529 Oct 24, 2025
0a9b8b8
wip
simo6529 Oct 24, 2025
585170b
wip
simo6529 Oct 25, 2025
23ccf7c
wip
simo6529 Oct 25, 2025
5064dd8
wip
simo6529 Oct 25, 2025
6563d84
wip
simo6529 Oct 25, 2025
e974751
wip
simo6529 Oct 26, 2025
79a4f49
wip
simo6529 Oct 26, 2025
ebbe291
wip
simo6529 Oct 26, 2025
dfc5500
wip
simo6529 Oct 26, 2025
069b1cb
wip
simo6529 Oct 26, 2025
3f1cee1
wip
simo6529 Oct 26, 2025
96aaaf0
wip
simo6529 Oct 26, 2025
38cb4be
wip
simo6529 Oct 26, 2025
9201f35
wip
simo6529 Oct 26, 2025
2fcda90
wip
simo6529 Oct 26, 2025
5af05fb
wip
simo6529 Oct 26, 2025
57d0aa4
wip
simo6529 Oct 26, 2025
40fa0b9
wip
simo6529 Oct 27, 2025
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
10 changes: 5 additions & 5 deletions __tests__/components/WaveGroupRemoveButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import WaveGroupRemoveButton from "@/components/waves/specs/groups/group/edit/WaveGroupRemoveButton";
import { WaveGroupType } from "@/components/waves/specs/groups/group/WaveGroup";
import { WaveGroupType } from "@/components/waves/specs/groups/group/WaveGroup.types";
import React from "react";

jest.mock("@/components/waves/specs/groups/group/edit/WaveGroupRemove", () =>
function MockRemove(props: any) {
return (
<div data-testid="modal" onClick={() => props.onEdit({})}>
<div data-testid="modal" onClick={() => props.onWaveUpdate({})}>
remove
</div>
);
Expand All @@ -17,13 +17,13 @@ jest.mock("@/components/waves/specs/groups/group/edit/WaveGroupRemove", () =>
describe("WaveGroupRemoveButton", () => {
it("opens modal and triggers edit", async () => {
const user = userEvent.setup();
const onEdit = jest.fn().mockResolvedValue(undefined);
const onWaveUpdate = jest.fn().mockResolvedValue(undefined);
render(
<WaveGroupRemoveButton wave={{} as any} type={WaveGroupType.VIEW} onEdit={onEdit} />
<WaveGroupRemoveButton wave={{} as any} type={WaveGroupType.VIEW} onWaveUpdate={onWaveUpdate} />
);
await user.click(screen.getByTitle("Remove"));
expect(screen.getByTestId("modal")).toBeInTheDocument();
await user.click(screen.getByTestId("modal"));
expect(onEdit).toHaveBeenCalled();
expect(onWaveUpdate).toHaveBeenCalled();
});
});
38 changes: 21 additions & 17 deletions __tests__/components/common/TabToggleWithOverflow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TabToggleWithOverflow } from '@/components/common/TabToggleWithOverflow';
Expand Down Expand Up @@ -28,13 +28,15 @@ describe('TabToggleWithOverflow', () => {
expect(screen.getByText('B')).toBeInTheDocument();

// open overflow dropdown
await user.click(screen.getByRole('button', { name: 'More' }));
const optionC = screen.getByRole('tab', { name: 'C' });
await user.click(screen.getByRole('button', { name: 'More tabs' }));
const optionC = screen.getByRole('menuitem', { name: 'C' });
expect(optionC).toBeInTheDocument();

await user.click(optionC);
expect(onSelect).toHaveBeenCalledWith('c');
expect(screen.queryByRole('tab', { name: 'C' })).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByRole('menuitem', { name: 'C' })).not.toBeInTheDocument()
);
});

it('shows active label when active tab is in overflow', () => {
Expand Down Expand Up @@ -83,26 +85,28 @@ describe('TabToggleWithOverflow', () => {
/>
);

const moreButton = screen.getByRole('button', { name: 'More' });
const moreButton = screen.getByRole('button', { name: 'More tabs' });
expect(moreButton).toHaveAttribute('aria-expanded', 'false');

moreButton.focus();
await user.tab(); // focus first tab
await user.tab(); // focus second tab
await user.tab(); // focus overflow trigger
await user.keyboard('{Enter}');
expect(moreButton).toHaveAttribute('aria-expanded', 'true');
const optionC = screen.getByRole('tab', { name: 'C' });
const optionC = screen.getByRole('menuitem', { name: 'C' });
expect(optionC).toBeInTheDocument();

optionC.focus();
await user.keyboard('{Escape}');
expect(moreButton).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('tab', { name: 'C' })).not.toBeInTheDocument();
await waitFor(() =>
expect(screen.queryByRole('menuitem', { name: 'C' })).not.toBeInTheDocument()
);

moreButton.focus();
await user.keyboard('{Space}');
await user.keyboard(' ');
expect(moreButton).toHaveAttribute('aria-expanded', 'true');
});

it('marks overflow tabs with aria-selected when opened', async () => {
it('indicates overflow active state via data attribute when opened', async () => {
const user = userEvent.setup();
render(
<TabToggleWithOverflow
Expand All @@ -113,15 +117,15 @@ describe('TabToggleWithOverflow', () => {
/>
);

const moreButton = screen.getByRole('button', { name: 'D' });
const moreButton = screen.getByRole('button', { name: 'More tabs' });
await user.click(moreButton);

expect(screen.getByRole('tab', { name: 'D' })).toHaveAttribute(
'aria-selected',
expect(screen.getByRole('menuitem', { name: 'D' })).toHaveAttribute(
'data-active',
'true'
);
expect(screen.getByRole('tab', { name: 'C' })).toHaveAttribute(
'aria-selected',
expect(screen.getByRole('menuitem', { name: 'C' })).toHaveAttribute(
'data-active',
'false'
);
});
Expand Down
2 changes: 1 addition & 1 deletion __tests__/components/groups/GroupCreateWallets.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ describe('GroupCreateWallets', () => {
const user = userEvent.setup();
const { setWallets } = renderComp({ walletsLimit:5 });
await user.click(screen.getByLabelText('Remove wallets'));
expect(setWallets).toHaveBeenLastCalledWith(null);
expect(setWallets).toHaveBeenCalledWith(null);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,20 @@ import { ReactQueryWrapperContext } from '@/components/react-query-wrapper/React
jest.mock('@/components/groups/page/create/actions/GroupCreateTest', () => () => <div data-testid="test" />);
jest.mock('@/components/distribution-plan-tool/common/CircleLoader', () => () => <div data-testid="loader" />);

const commonApiPost = jest.fn();
jest.mock('@/services/api/common-api', () => ({
commonApiPost: (...args: any[]) => commonApiPost(...args),
const mockSubmit = jest.fn();
const mockValidate = jest.fn();
jest.mock('@/hooks/groups/useGroupMutations', () => ({
useGroupMutations: () => ({
validate: mockValidate,
submit: mockSubmit,
runTest: jest.fn(),
isSubmitting: false,
isTesting: false,
updateVisibility: jest.fn(),
isUpdatingVisibility: false,
}),
}));

// simple useMutation mock that executes mutationFn and callbacks
const useMutationMock = jest.fn((options: any) => {
const mutateAsync = jest.fn(async (param?: any) => {
try {
const result = await options.mutationFn(param);
if (options.onSuccess) options.onSuccess(result, param, undefined);
if (options.onSettled) options.onSettled(result, undefined, param, undefined);
return result;
} catch (err) {
if (options.onError) options.onError(err, param, undefined);
if (options.onSettled) options.onSettled(undefined, err, param, undefined);
throw err;
}
});
return { mutateAsync };
});

jest.mock('@tanstack/react-query', () => ({ useMutation: (options: any) => useMutationMock(options) }));

const defaultGroup = {
name: '',
group: {
Expand Down Expand Up @@ -68,7 +58,14 @@ function renderActions(props?: Partial<React.ComponentProps<typeof GroupCreateAc
return { auth, queryCtx, onCompleted };
}

beforeEach(() => {
jest.clearAllMocks();
mockSubmit.mockReset();
mockValidate.mockReset();
});

it('disables create button when no filters selected', () => {
mockValidate.mockReturnValue({ valid: false, issues: [] });
renderActions();
expect(screen.getByRole('button', { name: 'Create' })).toBeDisabled();
});
Expand All @@ -80,20 +77,19 @@ it('creates group and marks visible on save', async () => {
group: { ...defaultGroup.group, identity_addresses: ['0x1'] },
};
const originalGroup = { id: 'old', created_by: { handle: 'Alice' } } as any;
commonApiPost.mockResolvedValueOnce({ id: '123' }).mockResolvedValueOnce({});
mockValidate.mockReturnValue({ valid: true, issues: [] });
mockSubmit.mockResolvedValueOnce({ ok: true, group: { id: '123' }, published: true });

const { auth, queryCtx, onCompleted } = renderActions({ groupConfig, originalGroup });

await userEvent.click(screen.getByRole('button', { name: 'Create' }));

await waitFor(() => expect(commonApiPost).toHaveBeenCalledTimes(2));
expect(auth.requestAuth).toHaveBeenCalled();
expect(commonApiPost).toHaveBeenNthCalledWith(1, { endpoint: 'groups', body: groupConfig });
expect(commonApiPost).toHaveBeenNthCalledWith(2, {
endpoint: 'groups/123/visible',
body: { visible: true, old_version_id: 'old' },
await waitFor(() => expect(mockSubmit).toHaveBeenCalledTimes(1));
expect(mockSubmit).toHaveBeenCalledWith({
payload: groupConfig,
previousGroup: originalGroup,
currentHandle: 'alice',
});
expect(auth.setToast).toHaveBeenCalledWith({ message: 'Group created.', type: 'success' });
expect(queryCtx.onGroupCreate).toHaveBeenCalled();
expect(onCompleted).toHaveBeenCalled();
});
Original file line number Diff line number Diff line change
@@ -1,76 +1,107 @@
// @ts-nocheck
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import GroupCreateTest from '@/components/groups/page/create/actions/GroupCreateTest';
import { AuthContext } from '@/components/auth/Auth';

const commonApiPost = jest.fn();
const commonApiFetch = jest.fn();
const hookState = {
runTest: jest.fn(),
isTesting: false,
submit: jest.fn(),
validate: jest.fn(),
updateVisibility: jest.fn(),
isUpdatingVisibility: false,
};

jest.mock('@/services/api/common-api', () => ({
commonApiPost: (...args: any[]) => commonApiPost(...args),
commonApiFetch: (...args: any[]) => commonApiFetch(...args),
jest.mock('@/hooks/groups/useGroupMutations', () => ({
useGroupMutations: () => hookState,
}));

const mutateAsyncMock = jest.fn();

const useMutationMock = jest.fn((options: any) => {
const mutateAsync = async (param?: any) => {
return options.mutationFn(param);
};
mutateAsyncMock.mockImplementation(mutateAsync);
return { mutateAsync: mutateAsyncMock };
});

const useQueryMock = jest.fn((...args: any[]) => ({ isFetching: false, data: undefined }));

const useQueryMock = jest.fn().mockReturnValue({ isFetching: false, data: undefined });
jest.mock('@tanstack/react-query', () => ({
useMutation: (opts: any) => useMutationMock(opts),
// @ts-expect-error - test mock
// @ts-expect-error - partial mock for tests
useQuery: (...args: any[]) => useQueryMock(...args),
keepPreviousData: {},
}));

const commonApiFetch = jest.fn();
jest.mock('@/services/api/common-api', () => ({
commonApiFetch: (...args: any[]) => commonApiFetch(...args),
}));

const defaultGroupConfig = {
name: 'My Group',
group: {
identity_addresses: [],
excluded_identity_addresses: [],
owns_nfts: [],
tdh: { min: null, max: null },
rep: { min: null, max: null, user_identity: null, category: null, direction: null },
cic: { min: null, max: null, user_identity: null, direction: null },
level: { min: null, max: null },
},
};

beforeEach(() => {
jest.clearAllMocks();
hookState.runTest.mockReset();
hookState.isTesting = false;
});

function renderComponent(props?: Partial<React.ComponentProps<typeof GroupCreateTest>>) {
const auth = {
requestAuth: jest.fn().mockResolvedValue({ success: true }),
setToast: jest.fn(),
connectedProfile: { handle: 'alice' },
} as any;

render(
<AuthContext.Provider value={auth}>
<GroupCreateTest
groupConfig={{ name: 'name', group: {} } as any}
disabled={false}
{...props}
/>
<GroupCreateTest groupConfig={defaultGroupConfig as any} disabled={false} {...props} />
</AuthContext.Provider>
);

return { auth };
}

beforeEach(() => {
jest.clearAllMocks();
});

test('disables button when disabled prop true', () => {
it('disables button when disabled prop true', () => {
render(
<AuthContext.Provider value={{} as any}>
<GroupCreateTest groupConfig={{} as any} disabled={true} />
<GroupCreateTest groupConfig={defaultGroupConfig as any} disabled />
</AuthContext.Provider>
);

expect(screen.getByRole('button', { name: 'Test' })).toBeDisabled();
});

test('calls mutation and displays loader on click', async () => {
const { auth } = renderComponent();
const button = screen.getByRole('button', { name: 'Test' });
it('calls runTest with payload and fallback name', async () => {
hookState.runTest.mockResolvedValueOnce({ ok: true, group: { id: '123' } });
const customConfig = {
...defaultGroupConfig,
name: '',
};

await fireEvent.click(button);
renderComponent({ groupConfig: customConfig as any });

await waitFor(() => expect(auth.requestAuth).toHaveBeenCalled());
expect(mutateAsyncMock).toHaveBeenCalledWith({
name: 'name',
group: {},
await fireEvent.click(screen.getByRole('button', { name: 'Test' }));

await waitFor(() => expect(hookState.runTest).toHaveBeenCalledTimes(1));
expect(hookState.runTest).toHaveBeenCalledWith({
payload: customConfig,
nameFallback: 'alice Test Run',
});
});

it('shows toast when runTest fails with api error', async () => {
hookState.runTest.mockResolvedValueOnce({
ok: false,
reason: 'api',
error: 'failed',
});

const { auth } = renderComponent();

await fireEvent.click(screen.getByRole('button', { name: 'Test' }));

await waitFor(() => expect(hookState.runTest).toHaveBeenCalled());
expect(auth.setToast).toHaveBeenCalledWith({ message: 'failed', type: 'error' });
});
Loading