| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- 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: <Inbox /> },
- { path: '/inbox/messages/:messageId', element: <Inbox /> },
- { path: '*', element: <div>other</div> }
- ], { initialEntries, initialIndex });
- }
- function renderRouter(router: ReturnType<typeof createInboxRouter>) {
- return render(
- <ConfigProvider theme={{ ...mailhubTheme, token: { ...mailhubTheme.token, motion: false } }}>
- <AntApp>
- <I18nProvider>
- <AppContext.Provider value={appContext}>
- <RouterProvider router={router} />
- </AppContext.Provider>
- </I18nProvider>
- </AntApp>
- </ConfigProvider>
- );
- }
- 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: `<message-${id}@example.test>`,
- 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: `<p>${subject}</p>`,
- 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)
- };
|