mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 07:37:15 +00:00
fix(inbounds): surface form validation errors (#6084)
* inbounds: surface form validation errors React Hook Form validation previously returned early without showing why an inbound save was blocked. Report the first field error and switch to the corresponding form tab so operators can correct it. * Fix inbound form tab error navigation Improve react-hook-form error traversal so validation stops on real `FieldError` leaves (detected by `type`) instead of any object with a `message`. This makes Save reliably jump to the tab containing the first invalid field and show the specific error, avoiding the previous generic/ambiguous invalid-state handling. --------- Co-authored-by: sonic <sonic@linux.do>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { screen, act, render, cleanup } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { screen, act, render, cleanup, fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import InboundFormModal from '@/pages/inbounds/form/InboundFormModal';
|
||||
import { DBInbound } from '@/models/dbinbound';
|
||||
import { ThemeProvider } from '@/hooks/useTheme';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import {
|
||||
renderWithProviders,
|
||||
fieldLabels,
|
||||
@@ -11,6 +12,19 @@ import {
|
||||
chooseSelectOption,
|
||||
} from './test-utils';
|
||||
|
||||
const { messageError } = vi.hoisted(() => ({ messageError: vi.fn() }));
|
||||
|
||||
vi.mock('antd', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('antd')>();
|
||||
return {
|
||||
...actual,
|
||||
message: {
|
||||
...actual.message,
|
||||
useMessage: () => [{ error: messageError }, null],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function renderModal() {
|
||||
return renderWithProviders(
|
||||
<InboundFormModal
|
||||
@@ -25,6 +39,63 @@ function renderModal() {
|
||||
);
|
||||
}
|
||||
|
||||
function primaryButton(): HTMLElement {
|
||||
const button = document.querySelector('.ant-modal-footer .ant-btn-primary');
|
||||
if (!button) throw new Error('Primary modal button not found');
|
||||
return button as HTMLElement;
|
||||
}
|
||||
|
||||
function cloneLikeVlessInbound(target: string) {
|
||||
return new DBInbound({
|
||||
id: 42,
|
||||
port: 41234,
|
||||
listen: '',
|
||||
protocol: 'vless',
|
||||
remark: 'source clone',
|
||||
enable: false,
|
||||
settings: {
|
||||
clients: [],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
fallbacks: [],
|
||||
},
|
||||
streamSettings: {
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
tcpSettings: { header: { type: 'none' } },
|
||||
realitySettings: {
|
||||
target,
|
||||
serverNames: ['example.com'],
|
||||
privateKey: 'test-private-key',
|
||||
shortIds: ['abcd'],
|
||||
settings: {
|
||||
publicKey: 'test-public-key',
|
||||
fingerprint: 'chrome',
|
||||
spiderX: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
sniffing: { enabled: false },
|
||||
nodeId: null,
|
||||
shareAddrStrategy: 'listen',
|
||||
shareAddr: '',
|
||||
});
|
||||
}
|
||||
|
||||
function renderCloneLikeEdit(dbInbound: DBInbound) {
|
||||
renderWithProviders(
|
||||
<InboundFormModal
|
||||
open
|
||||
mode="edit"
|
||||
dbInbound={dbInbound}
|
||||
dbInbounds={[dbInbound]}
|
||||
availableNodes={[]}
|
||||
onClose={() => {}}
|
||||
onSaved={() => {}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('InboundFormModal', () => {
|
||||
it('renders add mode without crashing', () => {
|
||||
renderModal();
|
||||
@@ -141,4 +212,37 @@ describe('InboundFormModal', () => {
|
||||
expect(strategyItem('Node address')).toBeTruthy();
|
||||
expect(strategyItem('Inbound listen')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('surfaces a Reality validation error and switches to its tab', async () => {
|
||||
const post = vi.mocked(HttpUtil.post);
|
||||
post.mockClear();
|
||||
messageError.mockClear();
|
||||
renderCloneLikeEdit(cloneLikeVlessInbound('example.com'));
|
||||
|
||||
fireEvent.click(primaryButton());
|
||||
|
||||
await waitFor(() => {
|
||||
const securityTab = screen.getByRole('tab', { name: 'Security' });
|
||||
expect(securityTab.getAttribute('aria-selected')).toBe('true');
|
||||
});
|
||||
expect(messageError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('REALITY target must include a port'),
|
||||
);
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('submits a valid clone-like Reality inbound', async () => {
|
||||
const post = vi.mocked(HttpUtil.post);
|
||||
post.mockClear();
|
||||
renderCloneLikeEdit(cloneLikeVlessInbound('example.com:443'));
|
||||
|
||||
fireEvent.click(primaryButton());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
'/panel/api/inbounds/update/42',
|
||||
expect.objectContaining({ enable: false, port: 41234, protocol: 'vless' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user