import { App as AntApp, ConfigProvider } from 'antd'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryRouter, RouterProvider, useLocation } 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 { api } from '../../src/frontend/services/api'; import { mailhubTheme } from '../../src/frontend/theme'; import type { RuntimeConfig } from '../../src/frontend/types'; import { AdminLayout } from '../../src/layouts/AdminLayout'; import Settings from '../../src/pages/Settings'; describe('Settings navigation guard', () => { afterEach(() => vi.restoreAllMocks()); it('guards sidebar and history navigation, beforeunload, and stops guarding after a successful save', async () => { const user = userEvent.setup(); vi.spyOn(api, 'adminSettings').mockResolvedValue({ settings: runtimeConfig }); const saveSettings = vi.spyOn(api, 'saveAdminSettings').mockImplementation(async (values) => ({ settings: { ...runtimeConfig, ...values } })); const router = renderSettingsRouter(); let baseUrlInput = await screen.findByLabelText('APP_BASE_URL'); const cleanUnload = new Event('beforeunload', { cancelable: true }); window.dispatchEvent(cleanUnload); expect(cleanUnload.defaultPrevented).toBe(false); await user.clear(baseUrlInput); await user.type(baseUrlInput, 'https://changed.example.test'); const dirtyUnload = new Event('beforeunload', { cancelable: true }); window.dispatchEvent(dirtyUnload); expect(dirtyUnload.defaultPrevented).toBe(true); await user.click(screen.getAllByText('发送活动')[0]); let dialog = await screen.findByRole('dialog', { name: '放弃未保存的更改?' }); expect(router.state.location.pathname).toBe('/settings'); await user.click(within(dialog).getByRole('button', { name: '继续编辑' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: '放弃未保存的更改?' })).toBeNull()); expect(router.state.location.pathname).toBe('/settings'); await user.click(screen.getAllByText('发送活动')[0]); dialog = await screen.findByRole('dialog', { name: '放弃未保存的更改?' }); await user.click(within(dialog).getByRole('button', { name: '离开页面' })); await waitFor(() => expect(router.state.location.pathname).toBe('/activity')); await act(async () => { await router.navigate(-1); }); baseUrlInput = await screen.findByLabelText('APP_BASE_URL'); await user.clear(baseUrlInput); await user.type(baseUrlInput, 'https://history.example.test'); await act(async () => { await router.navigate(-1); }); dialog = await screen.findByRole('dialog', { name: '放弃未保存的更改?' }); expect(router.state.location.pathname).toBe('/settings'); await user.click(within(dialog).getByRole('button', { name: '继续编辑' })); await waitFor(() => expect(screen.queryByRole('dialog', { name: '放弃未保存的更改?' })).toBeNull()); await act(async () => { await router.navigate(1); }); dialog = await screen.findByRole('dialog', { name: '放弃未保存的更改?' }); await user.click(within(dialog).getByRole('button', { name: '离开页面' })); await waitFor(() => expect(router.state.location.pathname).toBe('/activity')); await act(async () => { await router.navigate(-1); }); baseUrlInput = await screen.findByLabelText('APP_BASE_URL'); await user.clear(baseUrlInput); await user.type(baseUrlInput, 'https://saved.example.test'); const saveButton = screen.getByRole('button', { name: /保存设置/ }); await user.click(saveButton); await waitFor(() => expect(saveSettings).toHaveBeenCalledTimes(1)); await waitFor(() => expect((saveButton as HTMLButtonElement).disabled).toBe(true)); const savedUnload = new Event('beforeunload', { cancelable: true }); window.dispatchEvent(savedUnload); expect(savedUnload.defaultPrevented).toBe(false); await user.click(screen.getAllByText('概览')[0]); await waitFor(() => expect(router.state.location.pathname).toBe('/overview')); expect(screen.queryByRole('dialog', { name: '放弃未保存的更改?' })).toBeNull(); }); }); function renderSettingsRouter() { const context: AppContextValue = { user: { id: 1, username: 'operator', email: 'operator@example.test', role: 'admin', status: 'active' }, config: runtimeConfig, refreshBootstrap: vi.fn(async () => undefined), logout: vi.fn(async () => undefined) }; const router = createMemoryRouter([ { element: , children: [ { path: '/settings', element: }, { path: '*', element: } ] } ], { initialEntries: ['/overview', '/settings', '/activity'], initialIndex: 1 }); render( ); return router; } function LocationProbe() { const location = useLocation(); return
{location.pathname}
; } 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 };