mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 08:15:38 +00:00
fix: make Undo after Report spam put the message back and say Moved back, and name the Spam folder as the sidebar shows it
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { usePluginStore } from '@/stores/plugin-store';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
import { useToastStore } from '@/stores/toast-store';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
import { clearAllHooks } from '@/lib/plugin-hooks';
|
||||
|
||||
// Report spam on the open message offered an Undo that moved it to the Inbox
|
||||
// and then said "Email moved", where every other Undo puts the message back
|
||||
// in its own folders and says "Moved back".
|
||||
|
||||
vi.mock('next/dynamic', () => ({ default: (load: () => Promise<React.ComponentType<Record<string, unknown>>>) => {
|
||||
const Component = lazy(async () => ({ default: await load() }));
|
||||
return function Dynamic(props: Record<string, unknown>) {
|
||||
return <Suspense fallback={null}><Component {...props} /></Suspense>;
|
||||
};
|
||||
} }));
|
||||
vi.mock('@/components/email/email-viewer', () => ({
|
||||
EmailViewer: ({ onMarkAsSpam, onUndoSpam }: { onMarkAsSpam?: () => void; onUndoSpam?: () => void }) => (
|
||||
<>
|
||||
<button onClick={() => onMarkAsSpam?.()}>stub spam</button>
|
||||
<button onClick={() => onUndoSpam?.()}>stub not spam</button>
|
||||
</>
|
||||
),
|
||||
}));
|
||||
vi.mock('@/components/email/email-composer', () => ({ EmailComposer: () => null }));
|
||||
vi.mock('@/components/layout/sidebar', () => ({ Sidebar: () => null }));
|
||||
vi.mock('@/components/layout/navigation-rail', () => ({ NavigationRail: () => null }));
|
||||
vi.mock('@/components/layout/calendar-event-notification-toaster', () => ({ CalendarEventNotificationToaster: () => null }));
|
||||
vi.mock('@/components/layout/share-notification-toaster', () => ({ ShareNotificationToaster: () => null }));
|
||||
vi.mock('@/components/layout/mobile-header', () => ({ MobileHeader: () => null }));
|
||||
vi.mock('@/components/email/message-list-tabs', () => ({ MessageListTabs: () => null }));
|
||||
vi.mock('@/components/search/search-box', () => ({ SearchBox: () => null }));
|
||||
vi.mock('@/components/tour/tour-provider', () => ({ useTour: () => ({ activeTour: null, registerStep: vi.fn() }) }));
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({ usePluginSlotOffers: () => [] }));
|
||||
vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ appName: 'Mail' }) }));
|
||||
vi.mock('@/hooks/use-identity-sync', () => ({ useIdentitySync: () => {} }));
|
||||
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({ useProMultiAccountIdentities: () => [] }));
|
||||
vi.mock('@/hooks/use-pro-multi-account-mailboxes', () => ({ useProMultiAccountMailboxes: () => {} }));
|
||||
vi.mock('@/hooks/use-wopi-status', () => ({ useWopiStatus: () => null, canWopiOpen: () => false }));
|
||||
vi.mock('@/hooks/use-browser-navigation', () => ({ useBrowserNavigation: () => {} }));
|
||||
vi.mock('@/hooks/use-media-query', () => ({
|
||||
useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
|
||||
useMediaQuery: () => false,
|
||||
useIsDesktop: () => true,
|
||||
}));
|
||||
vi.mock('@/lib/push-bindings', () => ({ reconcilePushBindings: () => {}, releasePushBindings: () => {} }));
|
||||
vi.mock('@/components/email/email-list', () => ({ EmailList: () => <div>message list</div> }));
|
||||
|
||||
const { MailApp } = await import('../mail-app');
|
||||
|
||||
function mailbox(id: string, role: string): Mailbox {
|
||||
return {
|
||||
id, name: role, role, sortOrder: 0, totalEmails: 1, unreadEmails: 1, totalThreads: 1, unreadThreads: 1, isSubscribed: true,
|
||||
} as Mailbox;
|
||||
}
|
||||
const folders = [mailbox('inbox', 'inbox'), mailbox('trash', 'trash'), mailbox('junk', 'junk')];
|
||||
const unread = {
|
||||
id: 'm1', threadId: 't1', subject: 'Hello', receivedAt: '2026-10-26T10:00:00Z',
|
||||
keywords: {}, mailboxIds: { inbox: true },
|
||||
} as unknown as Email;
|
||||
|
||||
let client: Record<string, ReturnType<typeof vi.fn> | (() => unknown)>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Unexpected network request')));
|
||||
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} });
|
||||
usePluginStore.setState({ initializePlugins: vi.fn().mockResolvedValue(undefined) });
|
||||
useThemeStore.setState({ syncServerThemes: vi.fn().mockResolvedValue(undefined) });
|
||||
useToastStore.setState({ toasts: [] });
|
||||
clearAllHooks();
|
||||
client = {
|
||||
getAccountId: () => 'acc', getUsername: () => 'a@example.org',
|
||||
getSieveAccountId: () => null, hasDelayedSend: () => false, getSession: () => null,
|
||||
getMailboxes: vi.fn().mockResolvedValue(folders),
|
||||
moveToTrash: vi.fn().mockResolvedValue(undefined),
|
||||
moveEmail: vi.fn().mockResolvedValue(undefined),
|
||||
deleteEmail: vi.fn().mockResolvedValue(undefined),
|
||||
markAsSpam: vi.fn().mockResolvedValue(undefined),
|
||||
undoSpam: vi.fn().mockResolvedValue(undefined),
|
||||
restoreEmails: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
useAuthStore.setState({
|
||||
isAuthenticated: true, isLoading: false, client: client as never, activeAccountId: 'login-a', identities: [],
|
||||
getClientForAccount: (id: string) => (id === 'login-a' ? client : undefined) as never,
|
||||
getAllConnectedClients: () => new Map([['login-a', client]]) as never,
|
||||
});
|
||||
useSettingsStore.setState({ enableUnifiedMailbox: false, mailLayout: 'split', permanentlyDeleteJunk: false } as never);
|
||||
useEmailStore.setState({
|
||||
emails: [unread], selectedEmail: unread, selectedMailbox: 'inbox', selectedKeyword: null, mailboxes: folders,
|
||||
viewingAccountId: null, accountMailboxes: {}, isUnifiedView: false, unifiedRole: null,
|
||||
isScheduledView: false, isLoadingEmail: false, isLoading: false, searchQuery: '',
|
||||
searchFilters: { ...DEFAULT_SEARCH_FILTERS }, searchMailboxId: '',
|
||||
selectedEmailKeys: new Set(),
|
||||
fetchMailboxes: vi.fn().mockResolvedValue(undefined), fetchEmails: vi.fn().mockResolvedValue(undefined),
|
||||
fetchQuota: vi.fn().mockResolvedValue(undefined), fetchTagCounts: vi.fn().mockResolvedValue(undefined),
|
||||
refreshScheduledMetadata: vi.fn().mockResolvedValue(undefined),
|
||||
} as never);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const toastTitled = (title: string) => useToastStore.getState().toasts.find(t => t.title === title);
|
||||
|
||||
describe('Undo after Report spam', () => {
|
||||
it('puts the message back in its folder, unread, and says "Moved back"', async () => {
|
||||
render(<MailApp />);
|
||||
fireEvent.click(await screen.findByText('stub spam', {}, { timeout: 5000 }));
|
||||
await waitFor(() => expect(toastTitled('email_viewer.spam.toast_success')).toBeTruthy());
|
||||
const reported = toastTitled('email_viewer.spam.toast_success')!;
|
||||
expect(reported.action?.label).toBe('undo');
|
||||
reported.action!.onClick();
|
||||
await waitFor(() => expect(client.restoreEmails).toHaveBeenCalled());
|
||||
expect(client.restoreEmails).toHaveBeenCalledWith([{ id: 'm1', mailboxIds: { inbox: true }, seen: false }], undefined);
|
||||
expect(client.undoSpam).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(toastTitled('undo_done')).toBeTruthy());
|
||||
expect(toastTitled('notifications.email_moved')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('offers the same Undo after Not spam', async () => {
|
||||
useEmailStore.setState({ selectedMailbox: 'junk', emails: [{ ...unread, mailboxIds: { junk: true } }], selectedEmail: { ...unread, mailboxIds: { junk: true } } } as never);
|
||||
render(<MailApp />);
|
||||
fireEvent.click(await screen.findByText('stub not spam', {}, { timeout: 5000 }));
|
||||
await waitFor(() => expect(toastTitled('email_viewer.spam.toast_not_spam_success')).toBeTruthy());
|
||||
const restored = toastTitled('email_viewer.spam.toast_not_spam_success')!;
|
||||
expect(restored.action?.label).toBe('undo');
|
||||
restored.action!.onClick();
|
||||
await waitFor(() => expect(client.restoreEmails).toHaveBeenCalledWith([{ id: 'm1', mailboxIds: { junk: true }, seen: false }], undefined));
|
||||
});
|
||||
});
|
||||
@@ -3050,25 +3050,12 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
const handleMarkAsSpam = async (emailToMark: Email | null = selectedEmail) => {
|
||||
if (!client || !emailToMark) return;
|
||||
|
||||
const emailId = emailToMark.id;
|
||||
|
||||
// Undo puts it back in its own folders, read state included, as every
|
||||
// other Undo does.
|
||||
const restore = restoreBatchesFor([emailToMark], client);
|
||||
try {
|
||||
if (!(await markAsSpam(client, emailId, emailKeyFor(emailToMark)))) return;
|
||||
|
||||
toast.success(t('email_viewer.spam.toast_success'), {
|
||||
action: {
|
||||
label: t('email_viewer.spam.toast_undo'),
|
||||
onClick: async () => {
|
||||
try {
|
||||
if (await undoSpam(client, emailId, emailKeyFor(emailToMark))) toast.success(t('notifications.email_moved'));
|
||||
} catch (_error) {
|
||||
console.error("Failed to undo spam:", _error);
|
||||
toast.error(t('email_viewer.spam.error'));
|
||||
}
|
||||
},
|
||||
},
|
||||
duration: 5000,
|
||||
});
|
||||
if (!(await markAsSpam(client, emailToMark.id, emailKeyFor(emailToMark)))) return;
|
||||
toastWithUndo(t('email_viewer.spam.toast_success'), undoMoveAction(client, restore, undo));
|
||||
} catch (_error) {
|
||||
console.error("Failed to mark as spam:", _error);
|
||||
toast.error(t('email_viewer.spam.error'));
|
||||
@@ -3078,10 +3065,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
const handleUndoSpam = async (emailToRestore: Email | null = selectedEmail) => {
|
||||
if (!client || !emailToRestore) return;
|
||||
|
||||
const restore = restoreBatchesFor([emailToRestore], client);
|
||||
try {
|
||||
if (!(await undoSpam(client, emailToRestore.id, emailKeyFor(emailToRestore)))) return;
|
||||
|
||||
toast.success(t('email_viewer.spam.toast_not_spam_success'));
|
||||
toastWithUndo(t('email_viewer.spam.toast_not_spam_success'), undoMoveAction(client, restore, undo));
|
||||
} catch (_error) {
|
||||
console.error("Failed to restore email:", _error);
|
||||
toast.error(t('email_viewer.spam.error_not_spam'));
|
||||
|
||||
@@ -555,8 +555,8 @@
|
||||
"spam": {
|
||||
"button_title": "Denuncia com a brossa",
|
||||
"not_spam_title": "Marca com a legítim",
|
||||
"toast_success": "S'ha mogut a la brossa",
|
||||
"toast_batch": "{count} correus moguts a la brossa",
|
||||
"toast_success": "S'ha mogut a Correu brossa",
|
||||
"toast_batch": "{count} correus moguts a Correu brossa",
|
||||
"toast_undo": "Desfés",
|
||||
"toast_not_spam_success": "S'ha mogut a la safata d'entrada",
|
||||
"toast_not_spam_batch": "{count} correus moguts a la safata d'entrada",
|
||||
|
||||
@@ -555,8 +555,8 @@
|
||||
"spam": {
|
||||
"button_title": "Report spam",
|
||||
"not_spam_title": "Not spam",
|
||||
"toast_success": "Moved to Junk",
|
||||
"toast_batch": "{count} emails moved to Junk",
|
||||
"toast_success": "Moved to Spam",
|
||||
"toast_batch": "{count} emails moved to Spam",
|
||||
"toast_undo": "Undo",
|
||||
"toast_not_spam_success": "Moved to Inbox",
|
||||
"toast_not_spam_batch": "{count} emails moved to Inbox",
|
||||
|
||||
@@ -528,8 +528,8 @@
|
||||
"spam": {
|
||||
"button_title": "Reportar spam",
|
||||
"not_spam_title": "Marcar como legítimo",
|
||||
"toast_success": "Movido a Correo no deseado",
|
||||
"toast_batch": "{count} correos movidos a Correo no deseado",
|
||||
"toast_success": "Movido a Spam",
|
||||
"toast_batch": "{count} correos movidos a Spam",
|
||||
"toast_undo": "Deshacer",
|
||||
"toast_not_spam_success": "Movido a Bandeja de entrada",
|
||||
"toast_not_spam_batch": "{count} correos movidos a Bandeja de entrada",
|
||||
|
||||
@@ -528,8 +528,8 @@
|
||||
"spam": {
|
||||
"button_title": "Signaler comme spam",
|
||||
"not_spam_title": "Marquer comme légitime",
|
||||
"toast_success": "Déplacé vers Indésirables",
|
||||
"toast_batch": "{count} e-mails déplacés vers Indésirables",
|
||||
"toast_success": "Déplacé vers Spam",
|
||||
"toast_batch": "{count} e-mails déplacés vers Spam",
|
||||
"toast_undo": "Annuler",
|
||||
"toast_not_spam_success": "Déplacé vers Boîte de réception",
|
||||
"toast_not_spam_batch": "{count} e-mails déplacés vers Boîte de réception",
|
||||
|
||||
@@ -477,8 +477,8 @@
|
||||
"spam": {
|
||||
"button_title": "דווח על ספאם",
|
||||
"not_spam_title": "סמן כלגיטימי",
|
||||
"toast_success": "עבר לזבל",
|
||||
"toast_batch": "הודעות דוא\"ל{count}הועברו ל-Junk",
|
||||
"toast_success": "הועבר לספאם",
|
||||
"toast_batch": "{count} הודעות הועברו לספאם",
|
||||
"toast_undo": "בטל",
|
||||
"toast_not_spam_success": "הועבר לתיבת הדואר הנכנס",
|
||||
"toast_not_spam_batch": "הודעות דוא\"ל{count}הועברו ל-Inbox",
|
||||
|
||||
@@ -528,8 +528,8 @@
|
||||
"spam": {
|
||||
"button_title": "Segnala come spam",
|
||||
"not_spam_title": "Segna come legittimo",
|
||||
"toast_success": "Spostato in Posta indesiderata",
|
||||
"toast_batch": "{count} messaggi spostati in Posta indesiderata",
|
||||
"toast_success": "Spostato in Spam",
|
||||
"toast_batch": "{count} messaggi spostati in Spam",
|
||||
"toast_undo": "Annulla",
|
||||
"toast_not_spam_success": "Spostato in Posta in arrivo",
|
||||
"toast_not_spam_batch": "{count} messaggi spostati in Posta in arrivo",
|
||||
|
||||
@@ -555,8 +555,8 @@
|
||||
"spam": {
|
||||
"button_title": "Спам мэдээлэх",
|
||||
"not_spam_title": "Хууль ёсны гэж тэмдэглэх",
|
||||
"toast_success": "Хог хаяг руу шилжсэн",
|
||||
"toast_batch": "{count} имэйлийг хог хаяг руу зөөсөн",
|
||||
"toast_success": "Спам руу шилжсэн",
|
||||
"toast_batch": "{count} имэйлийг Спам руу зөөсөн",
|
||||
"toast_undo": "Буцаах",
|
||||
"toast_not_spam_success": "Ирсэн имэйл рүү шилжсэн",
|
||||
"toast_not_spam_batch": "{count} имэйлийг Ирсэн имэйл рүү зөөсөн",
|
||||
|
||||
@@ -528,8 +528,8 @@
|
||||
"spam": {
|
||||
"button_title": "Spam melden",
|
||||
"not_spam_title": "Markeren als legitiem",
|
||||
"toast_success": "Verplaatst naar Ongewenst",
|
||||
"toast_batch": "{count} e-mails verplaatst naar Ongewenst",
|
||||
"toast_success": "Verplaatst naar Spam",
|
||||
"toast_batch": "{count} e-mails verplaatst naar Spam",
|
||||
"toast_undo": "Ongedaan maken",
|
||||
"toast_not_spam_success": "Verplaatst naar Postvak IN",
|
||||
"toast_not_spam_batch": "{count} e-mails verplaatst naar Postvak IN",
|
||||
|
||||
@@ -555,10 +555,10 @@
|
||||
"spam": {
|
||||
"button_title": "Raportează spamul",
|
||||
"not_spam_title": "Marcați ca legitim",
|
||||
"toast_success": "Mutate în folderul „Junk”",
|
||||
"toast_batch": "{count} e-mailuri mutate în folderul „Junk”",
|
||||
"toast_success": "Mutat în Spam",
|
||||
"toast_batch": "{count} e-mailuri mutate în Spam",
|
||||
"toast_undo": "Anulează",
|
||||
"toast_not_spam_success": "Mutată în Inbox",
|
||||
"toast_not_spam_success": "Mutat în Primite",
|
||||
"toast_not_spam_batch": "{count} e-mailuri mutate în Inbox",
|
||||
"error": "Nu s-a reușit raportarea spamului",
|
||||
"error_not_spam": "Nu s-a putut restaura e-mailul"
|
||||
|
||||
@@ -530,8 +530,8 @@
|
||||
"spam": {
|
||||
"button_title": "Spam olarak bildir",
|
||||
"not_spam_title": "Meşru olarak işaretle",
|
||||
"toast_success": "Önemsiz'e taşındı",
|
||||
"toast_batch": "{count} e-posta Önemsiz'e taşındı",
|
||||
"toast_success": "Spam'e taşındı",
|
||||
"toast_batch": "{count} e-posta Spam'e taşındı",
|
||||
"toast_undo": "Geri al",
|
||||
"toast_not_spam_success": "Gelen Kutusu'na taşındı",
|
||||
"toast_not_spam_batch": "{count} e-posta Gelen Kutusu'na taşındı",
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
"spam": {
|
||||
"button_title": "Повідомити про спам",
|
||||
"not_spam_title": "Позначити як законне",
|
||||
"toast_success": "Перенесено в сміття",
|
||||
"toast_success": "Перенесено до спаму",
|
||||
"toast_batch": "{count} електронних листів переміщено до спаму",
|
||||
"toast_undo": "Скасувати",
|
||||
"toast_not_spam_success": "Переміщено до папки \"Вхідні\".",
|
||||
|
||||
Reference in New Issue
Block a user