import { App as AntApp, ConfigProvider } from 'antd'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AppContext, type AppContextValue } from '../../src/frontend/app-context'; import { I18nProvider } from '../../src/frontend/i18n/react'; import { detailHistoryState } from '../../src/frontend/navigation-state'; import { api } from '../../src/frontend/services/api'; import { mailhubTheme } from '../../src/frontend/theme'; import type { InboundMailbox, InboundMessage, RuntimeConfig } from '../../src/frontend/types'; import Inbox from '../../src/pages/Inbox'; describe('Inbox detail return path', () => { afterEach(() => vi.restoreAllMocks()); it('keeps the original list history when another message is selected from an open detail', async () => { const user = userEvent.setup(); const first = messageFixture(9, 1, 'Sent', 'First message'); const second = messageFixture(10, 1, 'Sent', 'Second message'); mockInboxApis([mailboxFixture(1)], [first, second]); vi.spyOn(api, 'inboundMessage').mockImplementation(async (id) => ({ message: id === second.id ? second : first })); const listPath = '/inbox?mailboxId=1&folder=Sent&page=2'; const router = createInboxRouter(['/overview', listPath], 1); renderRouter(router); await user.click(await screen.findByRole('button', { name: 'First message · sender@example.test' })); await waitFor(() => expect(router.state.location.pathname).toBe('/inbox/messages/9')); expect(detailHistoryState(router.state.location.state)).toEqual({ listPath, depth: 1, origin: 'list' }); fireEvent.click(screen.getByRole('button', { name: 'Second message · sender@example.test' })); await waitFor(() => expect(router.state.location.pathname).toBe('/inbox/messages/10')); expect(detailHistoryState(router.state.location.state)).toEqual({ listPath, depth: 2, origin: 'list' }); fireEvent.click(screen.getByRole('button', { name: 'Close' })); await waitFor(() => expect(`${router.state.location.pathname}${router.state.location.search}`).toBe(listPath)); await act(async () => { await router.navigate(-1); }); expect(router.state.location.pathname).toBe('/overview'); }); it('keeps direct detail tabs navigable and closes them without reopening on Back', async () => { const user = userEvent.setup(); const deepLinked = messageFixture(20, 2, 'Archive', 'Archived message'); const messages = vi.fn(async () => ({ messages: [deepLinked], total: 1, page: 1, pageSize: 25 })); mockInboxApis([mailboxFixture(1), mailboxFixture(2)], [deepLinked]); vi.spyOn(api, 'inboundMessages').mockImplementation(messages); vi.spyOn(api, 'inboundMessage').mockResolvedValue({ message: deepLinked }); const router = createInboxRouter(['/overview', '/inbox/messages/20'], 1); renderRouter(router); expect(await screen.findByText('Archived message')).toBeTruthy(); await waitFor(() => { const params = new URLSearchParams(router.state.location.search); expect(params.get('mailboxId')).toBe('2'); expect(params.get('folder')).toBe('Archive'); }); await waitFor(() => expect(messages).toHaveBeenCalledWith(expect.objectContaining({ mailboxId: 2, folder: 'Archive' }))); await user.click(screen.getByRole('tab', { name: 'HTML 源码' })); await waitFor(() => expect(new URLSearchParams(router.state.location.search).get('tab')).toBe('html')); expect(detailHistoryState(router.state.location.state)).toEqual({ listPath: '/inbox?mailboxId=2&folder=Archive', depth: 1, origin: 'direct' }); await act(async () => { await router.navigate(-1); }); expect(router.state.location.pathname).toBe('/inbox/messages/20'); expect(new URLSearchParams(router.state.location.search).has('tab')).toBe(false); expect(detailHistoryState(router.state.location.state)).toBeNull(); await act(async () => { await router.navigate(1); }); expect(new URLSearchParams(router.state.location.search).get('tab')).toBe('html'); expect(detailHistoryState(router.state.location.state)?.origin).toBe('direct'); fireEvent.click(screen.getByRole('button', { name: 'Close' })); await waitFor(() => expect(router.state.location.pathname).toBe('/inbox')); const params = new URLSearchParams(router.state.location.search); expect(params.get('mailboxId')).toBe('2'); expect(params.get('folder')).toBe('Archive'); await act(async () => { await router.navigate(-1); }); expect(router.state.location.pathname).toBe('/overview'); expect(router.state.location.pathname).not.toContain('/messages/'); }); }); function createInboxRouter(initialEntries: string[], initialIndex: number) { return createMemoryRouter([ { path: '/inbox', element: }, { path: '/inbox/messages/:messageId', element: }, { path: '*', element:
other
} ], { initialEntries, initialIndex }); } function renderRouter(router: ReturnType) { return render( ); } function mockInboxApis(mailboxes: InboundMailbox[], messages: InboundMessage[]) { vi.spyOn(api, 'domains').mockResolvedValue({ domains: [] }); vi.spyOn(api, 'inboundMailboxes').mockResolvedValue({ mailboxes }); vi.spyOn(api, 'inboundFolders').mockImplementation(async (mailboxId) => ({ folders: [ { name: 'INBOX', specialUse: null, messageCount: 0, unreadCount: 0 }, { name: 'Sent', specialUse: '\\Sent', messageCount: mailboxId === 1 ? messages.length : 0, unreadCount: 0 }, { name: 'Archive', specialUse: '\\Archive', messageCount: mailboxId === 2 ? messages.length : 0, unreadCount: 0 } ] })); vi.spyOn(api, 'inboundMessages').mockResolvedValue({ messages, total: messages.length, page: 1, pageSize: 25 }); } function mailboxFixture(id: number): InboundMailbox { return { id, userId: 1, domainId: id, domain: `example-${id}.test`, address: `inbox-${id}@example-${id}.test`, localPart: `inbox-${id}`, displayName: `Inbox ${id}`, aliases: [], forwardTo: [], keepForwarded: true, quotaMb: 1024, passwordSet: true, passwordRecoverable: false, status: 'active', messageCount: 2, unreadCount: 0, createdAt: '2026-07-14T00:00:00.000Z', updatedAt: '2026-07-14T00:00:00.000Z' }; } function messageFixture(id: number, mailboxId: number, folder: string, subject: string): InboundMessage { return { id, mailboxId, userId: 1, domainId: mailboxId, domain: `example-${mailboxId}.test`, mailboxAddress: `inbox-${mailboxId}@example-${mailboxId}.test`, folder, sender: 'sender@example.test', recipients: [`inbox-${mailboxId}@example-${mailboxId}.test`], subject, messageId: ``, preview: `${subject} preview`, read: true, receivedAt: '2026-07-14T00:00:00.000Z', createdAt: '2026-07-14T00:00:00.000Z', updatedAt: '2026-07-14T00:00:00.000Z', textBody: `${subject} body`, htmlBody: `

${subject}

`, rawMessage: `Subject: ${subject}` }; } const runtimeConfig: RuntimeConfig = { appBaseUrl: 'https://mail.example.test', mailHostname: 'mail.example.test', sendingIp: '192.0.2.10', defaultSpfMechanisms: '', dmarcPolicy: 'none', dmarcRua: '', sendRequiresVerified: true, engagementTrackingEnabled: true, listUnsubscribeMailto: '', listUnsubscribeUrl: '', listUnsubscribePostEnabled: false, feedbackIdEnabled: false, reportAbuseTo: '', csaComplaintsTo: '', bounceAddress: '', bounceEnvelopeEnabled: false }; const appContext: AppContextValue = { user: { id: 1, username: 'admin', email: 'admin@example.test', role: 'admin', status: 'active' }, config: runtimeConfig, refreshBootstrap: vi.fn(async () => undefined), logout: vi.fn(async () => undefined) };