mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-16 01:26:07 +00:00
fix(ui): stop the sniffing form island from clobbering unrendered fields
antd's Form.useWatch only reports registered fields, so while the
sniffing toggle was off the island emitted { enabled: false } upward and
replaced the full Sniffing object in form state. Saving a VLESS reverse
outbound then crashed in sniffingToWire on the missing ipsExcluded
array; the loopback outbound and the inbound sniffing tab shared the
same hole. Watch the store with preserve: true so unrendered fields
keep their values, and seed a missing value from the schema defaults
instead of an empty cast.
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { Form } from 'antd';
|
||||
|
||||
import SniffingFields from '@/lib/xray/forms/SniffingFields';
|
||||
import type { Sniffing } from '@/schemas/primitives/sniffing';
|
||||
import { SniffingSchema, type Sniffing } from '@/schemas/primitives/sniffing';
|
||||
|
||||
interface SniffingFieldProps {
|
||||
value?: Sniffing;
|
||||
@@ -12,12 +12,12 @@ interface SniffingFieldProps {
|
||||
|
||||
export default function SniffingField({ value, onChange, enableLabel }: SniffingFieldProps) {
|
||||
const [form] = Form.useForm();
|
||||
const [initial] = useState(() => value ?? ({} as Sniffing));
|
||||
const [initial] = useState(() => value ?? SniffingSchema.parse({}));
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
const lastEmitted = useRef(JSON.stringify(initial));
|
||||
|
||||
const sniffing = Form.useWatch('sniffing', form) as Sniffing | undefined;
|
||||
const sniffing = Form.useWatch('sniffing', { form, preserve: true }) as Sniffing | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (sniffing === undefined) return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
import OutboundFormModal from '@/pages/xray/outbounds/OutboundFormModal';
|
||||
import {
|
||||
@@ -54,4 +54,41 @@ describe('OutboundFormModal', () => {
|
||||
expect(labelsByProto.wireguard).not.toContain('Encryption');
|
||||
}
|
||||
}, 30000); // iterates every protocol, re-rendering a heavy modal each time — slow on CI runners
|
||||
|
||||
it('saves a vless reverse outbound while reverse sniffing stays disabled', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
renderWithProviders(
|
||||
<OutboundFormModal
|
||||
open
|
||||
outbound={{
|
||||
protocol: 'vless',
|
||||
tag: 'reverse-out',
|
||||
settings: {
|
||||
vnext: [{
|
||||
address: 'example.com',
|
||||
port: 443,
|
||||
users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
|
||||
}],
|
||||
reverse: { tag: 'r1', sniffing: {} },
|
||||
},
|
||||
streamSettings: { network: 'tcp', security: 'none' },
|
||||
}}
|
||||
existingTags={[]}
|
||||
onClose={() => {}}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
|
||||
|
||||
const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
|
||||
expect(ok).toBeTruthy();
|
||||
await act(async () => { fireEvent.click(ok); });
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
const payload = onConfirm.mock.calls[0][0] as {
|
||||
settings: { reverse?: { tag?: string } };
|
||||
};
|
||||
expect(payload.settings.reverse?.tag).toBe('r1');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user