diff --git a/src/components/ChannelListItem/__tests__/ChannelListItemActionButtons.defaults.test.tsx b/src/components/ChannelListItem/__tests__/ChannelListItemActionButtons.defaults.test.tsx
new file mode 100644
index 0000000000..28361a80f7
--- /dev/null
+++ b/src/components/ChannelListItem/__tests__/ChannelListItemActionButtons.defaults.test.tsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
+
+import { Chat } from '../../Chat';
+import { ChannelListItem } from '../ChannelListItem';
+import {
+ generateMember,
+ generateUser,
+ initClientWithChannels,
+} from '../../../mock-builders';
+import { ResizeObserverMock } from '../../../mock-builders/browser';
+
+const ResizeObserverConstructor =
+ ResizeObserverMock as unknown as typeof window.ResizeObserver;
+
+describe('ChannelListItemActionButtons defaults', () => {
+ const alice = generateUser({ id: 'alice-id' });
+ const bob = generateUser({ id: 'bob-id' });
+
+ const ownCapabilities = [
+ 'ban-channel-members',
+ 'leave-channel',
+ 'mute-channel',
+ 'read-events',
+ 'send-message',
+ ];
+
+ beforeEach(() => {
+ window.ResizeObserver = ResizeObserverConstructor;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ delete window.ResizeObserver;
+ });
+
+ const setupTwoMemberGroupChannel = async () => {
+ const { channels, client } = await initClientWithChannels({
+ channelsData: [
+ {
+ channel: {
+ id: 'two-member-group',
+ member_count: 2,
+ own_capabilities: ownCapabilities,
+ },
+ members: [generateMember({ user: alice }), generateMember({ user: bob })],
+ messages: [],
+ },
+ ],
+ customUser: alice,
+ });
+ return { channel: channels[0], client };
+ };
+
+ it('mutes channel from quick action with success notification', async () => {
+ const { channel, client } = await setupTwoMemberGroupChannel();
+ vi.spyOn(channel, 'mute').mockResolvedValue(undefined);
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ act(() => {
+ render(
+
+
+ ,
+ );
+ });
+
+ act(() => {
+ fireEvent.click(screen.getByRole('button', { name: 'Mute' }));
+ });
+ await waitFor(() => {
+ expect(channel.mute).toHaveBeenCalledTimes(1);
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Channel muted',
+ options: expect.objectContaining({ severity: 'success' }),
+ }),
+ );
+ });
+ });
+
+ it('shows mute error notification when mute fails', async () => {
+ const { channel, client } = await setupTwoMemberGroupChannel();
+ vi.spyOn(channel, 'mute').mockRejectedValueOnce(new Error('mute failed'));
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ act(() => {
+ render(
+
+
+ ,
+ );
+ });
+
+ act(() => {
+ fireEvent.click(screen.getByRole('button', { name: 'Mute' }));
+ });
+
+ await waitFor(() => {
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Failed to update channel mute status',
+ options: expect.objectContaining({ severity: 'error' }),
+ }),
+ );
+ });
+ });
+
+ it('blocks the other member from dropdown', async () => {
+ const { channel, client } = await setupTwoMemberGroupChannel();
+ vi.spyOn(channel, 'banUser').mockResolvedValue(undefined);
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ act(() => {
+ render(
+
+
+ ,
+ );
+ });
+
+ const toggle = document.querySelector(
+ '.str-chat__channel-list-item__action-buttons .str-chat__button--circular',
+ ) as HTMLButtonElement;
+ act(() => {
+ fireEvent.click(toggle);
+ });
+
+ const menu = document.querySelector('.str-chat__context-menu') as HTMLElement;
+ act(() => {
+ fireEvent.click(within(menu).getByRole('button', { name: 'Block User' }));
+ });
+
+ await waitFor(() => {
+ expect(channel.banUser).toHaveBeenCalledWith(bob.id, {});
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'User blocked',
+ options: expect.objectContaining({ severity: 'success' }),
+ }),
+ );
+ });
+ });
+
+ it('leaves channel from dropdown', async () => {
+ const { channel, client } = await setupTwoMemberGroupChannel();
+ vi.spyOn(channel, 'removeMembers').mockResolvedValue(undefined);
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ act(() => {
+ render(
+
+
+ ,
+ );
+ });
+
+ const toggle = document.querySelector(
+ '.str-chat__channel-list-item__action-buttons .str-chat__button--circular',
+ ) as HTMLButtonElement;
+ act(() => {
+ fireEvent.click(toggle);
+ });
+
+ const menu = document.querySelector('.str-chat__context-menu') as HTMLElement;
+ act(() => {
+ fireEvent.click(within(menu).getByRole('button', { name: 'Leave Channel' }));
+ });
+
+ await waitFor(() => {
+ expect(channel.removeMembers).toHaveBeenCalledWith([alice.id]);
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Left channel',
+ options: expect.objectContaining({ severity: 'success' }),
+ }),
+ );
+ });
+ });
+});
diff --git a/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx b/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx
new file mode 100644
index 0000000000..eac45420e6
--- /dev/null
+++ b/src/components/Message/hooks/__tests__/useReactionsFetcher.test.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { act, renderHook } from '@testing-library/react';
+
+import { useReactionsFetcher } from '../useReactionsFetcher';
+import { ChatProvider } from '../../../../context/ChatContext';
+import {
+ generateMessage,
+ getTestClientWithUser,
+ mockChatContext,
+} from '../../../../mock-builders';
+import type { LocalMessage, MessageResponse } from 'stream-chat';
+
+describe('useReactionsFetcher', () => {
+ it('paginates until next is empty', async () => {
+ const client = await getTestClientWithUser({ id: 'u1' });
+ const queryReactions = vi
+ .spyOn(client, 'queryReactions')
+ .mockResolvedValueOnce({
+ duration: '0',
+ next: 'page-2',
+ reactions: [{ created_at: new Date(), type: 'like', updated_at: new Date() }],
+ } as never)
+ .mockResolvedValueOnce({
+ duration: '0',
+ reactions: [{ created_at: new Date(), type: 'love', updated_at: new Date() }],
+ } as never);
+
+ const message = generateMessage() as MessageResponse & LocalMessage;
+
+ const wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+
+ const { result } = renderHook(() => useReactionsFetcher(message), { wrapper });
+
+ let reactions: unknown[];
+ await act(async () => {
+ reactions = await result.current();
+ });
+
+ expect(queryReactions).toHaveBeenCalledTimes(2);
+ expect(queryReactions.mock.calls[0]?.[3]).toEqual(
+ expect.objectContaining({ limit: 25, next: undefined }),
+ );
+ expect(queryReactions.mock.calls[1]?.[3]).toEqual(
+ expect.objectContaining({ limit: 25, next: 'page-2' }),
+ );
+ expect(reactions!).toHaveLength(2);
+ });
+});
diff --git a/src/components/MessageActions/__tests__/MessageActions.test.tsx b/src/components/MessageActions/__tests__/MessageActions.test.tsx
index bd940f5711..4fdcc3f8fd 100644
--- a/src/components/MessageActions/__tests__/MessageActions.test.tsx
+++ b/src/components/MessageActions/__tests__/MessageActions.test.tsx
@@ -1206,4 +1206,248 @@ describe('', () => {
expect(contextMenu).toBeInTheDocument();
});
});
+
+ describe('Default message actions edge coverage', () => {
+ it('should copy message text to clipboard when Copy is used', async () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.assign(navigator, { clipboard: { writeText } });
+ const message = generateMessage({ text: 'hello copy', user: alice });
+ await renderMessageActions({
+ customMessageContext: { message },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Copy Message'));
+ });
+ expect(writeText).toHaveBeenCalledWith('hello copy');
+ });
+
+ it('should call handleRetry when Resend is used on a failed message', async () => {
+ const handleRetry = vi.fn();
+ const message = generateMessage({
+ error: { status: 400 },
+ status: 'failed',
+ user: alice,
+ });
+ await renderMessageActions({
+ channelStateOpts: {
+ channelCapabilities: { 'send-message': true },
+ },
+ customMessageContext: { handleRetry, message },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Resend'));
+ });
+ expect(handleRetry).toHaveBeenCalledWith(message);
+ });
+
+ it('should call client.blockUser when Block User is used', async () => {
+ const otherUser = generateUser();
+ const message = generateMessage({ user: otherUser });
+ const chatClient = await getTestClientWithUser(alice);
+ const blockUserSpy = vi.spyOn(chatClient, 'blockUser').mockImplementation(() => {});
+ await renderMessageActions({
+ chatClient,
+ customMessageContext: { message },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Block User'));
+ });
+ expect(blockUserSpy).toHaveBeenCalledWith(otherUser.id);
+ });
+
+ it('should notify on delete failure from the confirmation modal', async () => {
+ const handleDelete = vi.fn().mockRejectedValueOnce(new Error('delete failed'));
+ const message = generateMessage({ user: alice });
+ const chatClient = await getTestClientWithUser(alice);
+ const addSpy = vi.spyOn(chatClient.notifications, 'add');
+ await renderMessageActions({
+ channelStateOpts: {
+ channelCapabilities: { 'delete-own-message': true },
+ },
+ chatClient,
+ customMessageContext: { handleDelete, message },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Delete message'));
+ });
+ await act(async () => {
+ await fireEvent.click(screen.getByTestId('delete-message-alert-delete-button'));
+ });
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'delete failed',
+ options: expect.objectContaining({ severity: 'error' }),
+ }),
+ );
+ });
+
+ it('should pass through non-Error pin failures for notifications', async () => {
+ const handlePin = vi.fn().mockRejectedValueOnce({ message: 'pin rejected' });
+ const message = generateMessage({ pinned: false, user: alice });
+ const chatClient = await getTestClientWithUser(alice);
+ const addSpy = vi.spyOn(chatClient.notifications, 'add');
+ await renderMessageActions({
+ channelStateOpts: {
+ channelCapabilities: { 'pin-message': true },
+ },
+ chatClient,
+ customMessageContext: { handlePin, message },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Pin'));
+ });
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Error pinning message',
+ options: expect.objectContaining({
+ originalError: expect.objectContaining({ message: 'pin rejected' }),
+ severity: 'error',
+ }),
+ }),
+ );
+ });
+
+ it('should return to parent menu from Remind me submenu header', async () => {
+ const client = await getTestClientWithUser(alice);
+ await renderMessageActions({
+ channelConfig: { replies: true, user_message_reminders: true },
+ channelStateOpts: {
+ channelCapabilities: { 'send-reply': true },
+ },
+ chatClient: client,
+ customMessageContext: {
+ getMessageActions: () => [
+ 'delete',
+ 'edit',
+ 'flag',
+ 'mute',
+ 'pin',
+ 'quote',
+ 'react',
+ 'remindMe',
+ 'reply',
+ 'saveForLater',
+ ],
+ },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Remind me'));
+ });
+ await act(async () => {
+ await fireEvent.click(
+ document.querySelector(
+ '.str-chat__context-menu__back-button',
+ ) as HTMLButtonElement,
+ );
+ });
+ expect(screen.getByText('Thread Reply')).toBeInTheDocument();
+ });
+
+ it('should surface string rejection from reminder upsert as notification message', async () => {
+ const client = await getTestClientWithUser(alice);
+ vi.spyOn(client.reminders, 'upsertReminder').mockRejectedValueOnce('string-fail');
+ const addNotificationSpy = vi.spyOn(client.notifications, 'add');
+ await renderMessageActions({
+ channelConfig: { user_message_reminders: true },
+ chatClient: client,
+ customMessageContext: {
+ getMessageActions: () => [
+ 'delete',
+ 'edit',
+ 'flag',
+ 'mute',
+ 'pin',
+ 'quote',
+ 'react',
+ 'remindMe',
+ 'reply',
+ 'saveForLater',
+ ],
+ },
+ });
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Remind me'));
+ });
+ const remindMeOption = document.querySelector(
+ '.str-chat__message-actions-box__submenu .str-chat__message-actions-list-item-button',
+ ) as HTMLButtonElement;
+ await act(async () => {
+ await fireEvent.click(remindMeOption);
+ });
+ expect(addNotificationSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Error setting reminder',
+ options: expect.objectContaining({
+ originalError: expect.objectContaining({ message: 'string-fail' }),
+ severity: 'error',
+ }),
+ }),
+ );
+ });
+
+ it('should focus thread textarea when quoting from a thread message', async () => {
+ const {
+ channels: [channel],
+ client,
+ } = await initClientWithChannels();
+ const message = generateMessage({ parent_id: 'parent-1', user: client.user });
+ const textarea = document.createElement('textarea');
+ textarea.className = 'str-chat__textarea__textarea';
+ const threadRoot = document.createElement('div');
+ threadRoot.className = 'str-chat__thread';
+ threadRoot.appendChild(textarea);
+ document.body.appendChild(threadRoot);
+ const focusSpy = vi.spyOn(textarea, 'focus');
+
+ await act(async () => {
+ await render(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ await toggleOpenMessageActions();
+ await act(async () => {
+ await fireEvent.click(screen.getByText('Quote Reply'));
+ });
+ expect(focusSpy).toHaveBeenCalled();
+ document.body.removeChild(threadRoot);
+ });
+ });
});
diff --git a/src/components/MessageComposer/hooks/__tests__/useSubmitHandler.test.tsx b/src/components/MessageComposer/hooks/__tests__/useSubmitHandler.test.tsx
new file mode 100644
index 0000000000..e4e43a0ca4
--- /dev/null
+++ b/src/components/MessageComposer/hooks/__tests__/useSubmitHandler.test.tsx
@@ -0,0 +1,104 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { useSubmitHandler } from '../useSubmitHandler';
+
+const submitMocks = vi.hoisted(() => {
+ const stateApi = () => ({
+ getLatestValue: () => ({}),
+ next: vi.fn(),
+ partialNext: vi.fn(),
+ });
+ const messageComposerMock = {
+ attachmentManager: { state: stateApi() },
+ channel: { cid: 'messaging:test', stopTyping: vi.fn().mockResolvedValue(undefined) },
+ clear: vi.fn(),
+ compose: vi.fn(),
+ config: { text: { publishTypingEvents: false } },
+ customDataManager: { state: stateApi() },
+ editedMessage: undefined as unknown,
+ linkPreviewsManager: { state: stateApi() },
+ pollComposer: { state: stateApi() },
+ state: stateApi(),
+ textComposer: { state: stateApi() },
+ };
+ return {
+ addNotification: vi.fn(),
+ editMessage: vi.fn(),
+ messageComposerMock,
+ sendMessage: vi.fn(),
+ };
+});
+
+vi.mock('../../../Notifications', () => ({
+ useNotificationApi: () => ({
+ addNotification: submitMocks.addNotification,
+ addSystemNotification: vi.fn(),
+ removeNotification: vi.fn(),
+ startNotificationTimeout: vi.fn(),
+ }),
+}));
+
+vi.mock('../../../../context/ChannelActionContext', () => ({
+ useChannelActionContext: () => ({
+ editMessage: submitMocks.editMessage,
+ sendMessage: submitMocks.sendMessage,
+ }),
+}));
+
+vi.mock('../../../../context/TranslationContext', () => ({
+ useTranslationContext: () => ({ t: (k: string) => k }),
+}));
+
+vi.mock('../useMessageComposerController', () => ({
+ useMessageComposerController: () => submitMocks.messageComposerMock,
+}));
+
+describe('useSubmitHandler', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ submitMocks.messageComposerMock.editedMessage = undefined;
+ vi.spyOn(submitMocks.messageComposerMock, 'compose')
+ .mockImplementation()
+ .mockResolvedValue({
+ localMessage: { id: 'lm1', type: 'regular' },
+ message: { id: 'm1', text: 'hi' },
+ sendOptions: {},
+ });
+ });
+
+ it('notifies when editMessage fails', async () => {
+ submitMocks.messageComposerMock.editedMessage = { id: 'e1' };
+ submitMocks.editMessage.mockRejectedValueOnce(new Error('edit failed'));
+
+ const { result } = renderHook(() => useSubmitHandler({}));
+
+ await act(async () => {
+ await result.current.handleSubmit();
+ });
+
+ expect(submitMocks.addNotification).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Edit message request failed',
+ severity: 'error',
+ }),
+ );
+ });
+
+ it('restores composer state and notifies when sendMessage fails', async () => {
+ submitMocks.sendMessage.mockRejectedValueOnce(new Error('send failed'));
+
+ const { result } = renderHook(() => useSubmitHandler({}));
+
+ await act(async () => {
+ await result.current.handleSubmit();
+ });
+
+ expect(submitMocks.messageComposerMock.state.next).toHaveBeenCalled();
+ expect(submitMocks.addNotification).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Send message request failed',
+ severity: 'error',
+ }),
+ );
+ });
+});
diff --git a/src/components/Notifications/__tests__/Notification.test.tsx b/src/components/Notifications/__tests__/Notification.test.tsx
new file mode 100644
index 0000000000..15576a3733
--- /dev/null
+++ b/src/components/Notifications/__tests__/Notification.test.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+
+import { Notification } from '../Notification';
+import { useNotificationApi } from '../hooks/useNotificationApi';
+import { TranslationProvider } from '../../../context';
+import { mockTranslationContextValue } from '../../../mock-builders';
+
+import type { Notification as NotificationModel } from 'stream-chat';
+
+vi.mock('../hooks/useNotificationApi', () => ({
+ useNotificationApi: vi.fn(),
+}));
+
+const mockedUseNotificationApi = vi.mocked(useNotificationApi);
+
+describe('Notification', () => {
+ const removeNotification = vi.fn();
+
+ beforeEach(() => {
+ mockedUseNotificationApi.mockReturnValue({
+ addNotification: vi.fn(),
+ addSystemNotification: vi.fn(),
+ removeNotification,
+ startNotificationTimeout: vi.fn(),
+ });
+ removeNotification.mockClear();
+ });
+
+ const baseNotification: NotificationModel = {
+ actions: undefined,
+ createdAt: Date.now(),
+ id: 'n1',
+ message: 'Hello',
+ origin: { emitter: 'test' },
+ severity: 'info',
+ };
+
+ it('removes by id when dismiss is triggered without onDismiss', () => {
+ render(
+ opts?.value ?? _k })}
+ >
+
+ ,
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
+ expect(removeNotification).toHaveBeenCalledWith('n1');
+ });
+
+ it('renders action buttons and invokes handler', () => {
+ const handler = vi.fn();
+ render(
+ opts?.value ?? _k })}
+ >
+
+ ,
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onDismiss when provided instead of removeNotification', () => {
+ const onDismiss = vi.fn();
+ render(
+ opts?.value ?? _k })}
+ >
+
+ ,
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ expect(removeNotification).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/components/Notifications/__tests__/NotificationList.test.tsx b/src/components/Notifications/__tests__/NotificationList.test.tsx
index 1478e42429..7c2c2d6089 100644
--- a/src/components/Notifications/__tests__/NotificationList.test.tsx
+++ b/src/components/Notifications/__tests__/NotificationList.test.tsx
@@ -215,6 +215,36 @@ describe('NotificationList', () => {
);
});
+ it('uses origin.context.entryDirection when metadata is absent', () => {
+ currentNotifications = [
+ {
+ ...notifications[0],
+ origin: {
+ context: { entryDirection: 'right' },
+ emitter: 'test',
+ },
+ },
+ ];
+
+ render();
+
+ expect(screen.getByTestId('notification-list')).toHaveClass(
+ 'str-chat__notification-list--enter-from-right',
+ );
+ expect(screen.getByTestId('notification-n-1')).toHaveAttribute(
+ 'data-entry-direction',
+ 'right',
+ );
+ });
+
+ it('supports top vertical alignment', () => {
+ render();
+
+ expect(screen.getByTestId('notification-list')).toHaveClass(
+ 'str-chat__notification-list--position-top',
+ );
+ });
+
it('uses custom Notification component from ComponentContext', () => {
const CustomNotification = React.forwardRef<
HTMLDivElement,
diff --git a/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx b/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx
new file mode 100644
index 0000000000..d6aa9366fb
--- /dev/null
+++ b/src/components/Poll/PollActions/__tests__/EndPollAlert.test.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
+
+import { EndPollAlert } from '../EndPollAlert';
+import {
+ ChatProvider,
+ ModalContextProvider,
+ PollProvider,
+ TranslationProvider,
+} from '../../../../context';
+import {
+ generatePoll,
+ generateUser,
+ getTestClientWithUser,
+ mockChatContext,
+ mockTranslationContextValue,
+} from '../../../../mock-builders';
+import { Poll } from 'stream-chat';
+
+describe('EndPollAlert', () => {
+ it('closes modal and notifies on successful poll end', async () => {
+ const user = generateUser();
+ const client = await getTestClientWithUser(user);
+ const pollData = generatePoll({ created_by_id: user.id, is_closed: false });
+ const poll = new Poll({ client, poll: pollData });
+ vi.spyOn(poll, 'close').mockResolvedValue(undefined);
+ const close = vi.fn();
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ render(
+
+ k })}>
+
+
+
+
+
+
+ ,
+ );
+
+ act(() => {
+ fireEvent.click(screen.getByTestId('end-poll-alert-end-vote-button'));
+ });
+
+ await waitFor(() => {
+ expect(poll.close).toHaveBeenCalledTimes(1);
+ expect(close).toHaveBeenCalledTimes(1);
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Poll ended',
+ options: expect.objectContaining({ severity: 'success' }),
+ }),
+ );
+ });
+ });
+
+ it('notifies on poll end failure without closing', async () => {
+ const user = generateUser();
+ const client = await getTestClientWithUser(user);
+ const pollData = generatePoll({ created_by_id: user.id, is_closed: false });
+ const poll = new Poll({ client, poll: pollData });
+ vi.spyOn(poll, 'close').mockRejectedValueOnce(new Error('end failed'));
+ const close = vi.fn();
+ const addSpy = vi.spyOn(client.notifications, 'add');
+
+ render(
+
+ k })}>
+
+
+
+
+
+
+ ,
+ );
+
+ act(() => {
+ fireEvent.click(screen.getByTestId('end-poll-alert-end-vote-button'));
+ });
+
+ await waitFor(() => {
+ expect(addSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Failed to end the poll',
+ options: expect.objectContaining({ severity: 'error' }),
+ }),
+ );
+ });
+ expect(close).not.toHaveBeenCalled();
+ });
+});