diff --git a/frontend/src/pages/hosts/HostFormModal.tsx b/frontend/src/pages/hosts/HostFormModal.tsx index 9089d711c..8c6b37b40 100644 --- a/frontend/src/pages/hosts/HostFormModal.tsx +++ b/frontend/src/pages/hosts/HostFormModal.tsx @@ -121,8 +121,7 @@ export default function HostFormModal({ watchedSnis && watchedSnis.filter((s) => typeof s === 'string' && s.trim() !== '').length > 1, ); - const watchedSingleSni = useWatch({ control: methods.control, name: 'sni' }) ?? ''; - const hostSni = (watchedSnis && watchedSnis.length > 0 ? watchedSnis[0] : '') || watchedSingleSni; + const hostSni = watchedSnis?.[0] ?? ''; const hostEchConfig = useWatch({ control: methods.control, name: 'echConfigList' }) ?? ''; const showTls = security === 'tls' || security === 'reality' || security === 'same'; const showTlsExtras = security === 'tls' || security === 'same'; @@ -208,11 +207,10 @@ export default function HostFormModal({ const { enable, ...rest } = values; const isDisabled = !enable; const cleanedSnis = (rest.snis || []).map((s) => s.trim()).filter(Boolean); - const primarySni = cleanedSnis.length > 0 ? cleanedSnis[0] : rest.sni || ''; const payload: BulkAddHostValues = { ...rest, hosts: (rest.hosts || []).filter((h) => h && h.trim() !== ''), - sni: primarySni, + sni: cleanedSnis[0] ?? '', snis: cleanedSnis, sniSharedRandom: cleanedSnis.length > 1 && Boolean(rest.sniSharedRandom), isDisabled, diff --git a/frontend/src/test/host-form-sni.test.tsx b/frontend/src/test/host-form-sni.test.tsx new file mode 100644 index 000000000..5e9420013 --- /dev/null +++ b/frontend/src/test/host-form-sni.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { HostRecord } from '@/api/queries/useHostsQuery'; +import HostFormModal from '@/pages/hosts/HostFormModal'; +import { HttpUtil, Msg } from '@/utils'; +import { renderWithProviders } from './test-utils'; + +const host = { + groupId: 'g1', + remark: 'DE WS', + inboundIds: [1], + hosts: ['cdn.example.com'], + security: 'same', + sni: 'dash.example.com', + snis: ['dash.example.com'], +} as HostRecord; + +describe('Host form SNI', () => { + beforeEach(() => { + vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', [])); + }); + + it('saves an empty SNI after the last SNI tag is removed', async () => { + const save = vi.fn().mockResolvedValue({ success: true }); + renderWithProviders( + {}} + />, + ); + const tag = await screen.findByTitle('dash.example.com'); + const remove = tag.querySelector('.ant-select-selection-item-remove'); + if (!remove) throw new Error('missing SNI tag remove button'); + fireEvent.click(remove); + await waitFor(() => expect(screen.queryByTitle('dash.example.com')).toBeNull()); + + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + await waitFor(() => expect(save).toHaveBeenCalledTimes(1)); + expect(save.mock.calls[0][0]).toMatchObject({ sni: '', snis: [] }); + }); +});