merge fix/settings-trim: dead settings, upstream remnants, JSON/Clash subscriptions removed
# Conflicts: # frontend/src/models/setting.ts # frontend/src/pages/settings/GeneralTab.tsx # frontend/src/pages/settings/SettingsPage.tsx # frontend/src/pages/settings/SubscriptionBalancersTab.tsx # frontend/src/pages/xray/outbounds/OutboundsTab.css # frontend/src/schemas/setting.ts
This commit is contained in:
@@ -19,7 +19,9 @@ describe('computeDefaultMasterUrl', () => {
|
||||
});
|
||||
|
||||
it('root base path yields the bare origin', () => {
|
||||
expect(computeDefaultMasterUrl('http://192.168.0.1:55889', '/')).toBe('http://192.168.0.1:55889');
|
||||
expect(computeDefaultMasterUrl('http://192.168.0.1:55889', '/')).toBe(
|
||||
'http://192.168.0.1:55889',
|
||||
);
|
||||
});
|
||||
|
||||
it('undefined or empty base path yields the bare origin', () => {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AllSettingSchema } from '@/schemas/setting';
|
||||
import { AllSetting } from '@/models/setting';
|
||||
|
||||
describe('ldapInsecureSkipVerify', () => {
|
||||
it('parses through the Zod schema', () => {
|
||||
const r = AllSettingSchema.safeParse({ ldapInsecureSkipVerify: true });
|
||||
expect(r.success).toBe(true);
|
||||
expect(r.success && r.data.ldapInsecureSkipVerify).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects non-boolean values', () => {
|
||||
expect(AllSettingSchema.safeParse({ ldapInsecureSkipVerify: 'yes' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults to false on the model and clones from payload', () => {
|
||||
expect(new AllSetting().ldapInsecureSkipVerify).toBe(false);
|
||||
expect(new AllSetting({ ldapInsecureSkipVerify: true }).ldapInsecureSkipVerify).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import SubBalancerFormModal from '@/pages/settings/SubBalancerFormModal';
|
||||
import type { SubBalancer } from '@/schemas/subBalancer';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
vi.mock('@/api/queries/useInboundOptions', () => ({
|
||||
useInboundOptions: () => ({
|
||||
data: [
|
||||
{ id: 1, tag: 'inb-vless', remark: 'First', protocol: 'vless', port: 443, enable: true },
|
||||
{ id: 2, tag: 'inb-ws', remark: 'Second', protocol: 'vmess', port: 8443, enable: true },
|
||||
{ id: 3, tag: 'inb-off', remark: 'Disabled', protocol: 'vless', port: 8080, enable: false },
|
||||
],
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
function renderModal(balancer: SubBalancer | null, onConfirm = vi.fn()) {
|
||||
renderWithProviders(
|
||||
<SubBalancerFormModal open balancer={balancer} onClose={() => {}} onConfirm={onConfirm} />,
|
||||
);
|
||||
return { onConfirm };
|
||||
}
|
||||
|
||||
function primaryButton(): HTMLElement {
|
||||
const btn = document.querySelector('.ant-modal-footer .ant-btn-primary');
|
||||
if (!btn) throw new Error('Primary button not found');
|
||||
return btn as HTMLElement;
|
||||
}
|
||||
|
||||
function erroredItemCount(): number {
|
||||
return document.querySelectorAll('.ant-form-item-has-error').length;
|
||||
}
|
||||
|
||||
function remarkInput(): HTMLInputElement {
|
||||
const el = Array.from(document.querySelectorAll('.ant-modal input')).find((i) =>
|
||||
(i as HTMLInputElement).placeholder.includes('Auto'),
|
||||
);
|
||||
if (!el) throw new Error('Remark input not found');
|
||||
return el as HTMLInputElement;
|
||||
}
|
||||
|
||||
function inboundOptionTitles(): string[] {
|
||||
const multi = document.querySelector('.ant-select-multiple');
|
||||
if (!multi) throw new Error('Inbound multi-select not found');
|
||||
fireEvent.mouseDown(multi as HTMLElement);
|
||||
return Array.from(document.querySelectorAll('.ant-select-item-option')).map((o) =>
|
||||
(o.getAttribute('title') ?? o.textContent ?? '').trim(),
|
||||
);
|
||||
}
|
||||
|
||||
function selectInbound(optionTitle: string) {
|
||||
const multi = document.querySelector('.ant-select-multiple');
|
||||
if (!multi) throw new Error('Inbound multi-select not found');
|
||||
// AntD 6 multiple selects have no .ant-select-selector; mousedown on the
|
||||
// root toggles the dropdown.
|
||||
fireEvent.mouseDown(multi as HTMLElement);
|
||||
const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find(
|
||||
(o) => (o.getAttribute('title') ?? o.textContent ?? '').trim() === optionTitle,
|
||||
);
|
||||
if (!option) throw new Error(`Option '${optionTitle}' not found`);
|
||||
fireEvent.click(option);
|
||||
fireEvent.keyDown(multi, { key: 'Escape' });
|
||||
}
|
||||
|
||||
describe('SubBalancerFormModal', () => {
|
||||
it('shows no validation errors when freshly opened in add mode', () => {
|
||||
renderModal(null);
|
||||
expect(document.querySelector('.ant-modal')).toBeTruthy();
|
||||
expect(erroredItemCount()).toBe(0);
|
||||
expect(primaryButton().hasAttribute('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('reveals required-field errors after a save attempt, without confirming', async () => {
|
||||
const { onConfirm } = renderModal(null);
|
||||
fireEvent.click(primaryButton());
|
||||
await waitFor(() => expect(erroredItemCount()).toBe(2));
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('confirms with parsed values once remark and an inbound are set', async () => {
|
||||
const { onConfirm } = renderModal(null);
|
||||
fireEvent.change(remarkInput(), { target: { value: ' auto ' } });
|
||||
selectInbound('First');
|
||||
fireEvent.click(primaryButton());
|
||||
await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1));
|
||||
expect(onConfirm).toHaveBeenCalledWith({
|
||||
remark: 'auto',
|
||||
strategy: 'random',
|
||||
inboundIds: [1],
|
||||
sortOrder: 1,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds the form from the edited balancer', async () => {
|
||||
const { onConfirm } = renderModal({
|
||||
id: 7,
|
||||
remark: 'existing',
|
||||
strategy: 'leastPing',
|
||||
inboundIds: [2],
|
||||
sortOrder: 3,
|
||||
enabled: false,
|
||||
});
|
||||
expect(remarkInput().value).toBe('existing');
|
||||
fireEvent.click(primaryButton());
|
||||
await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1));
|
||||
expect(onConfirm).toHaveBeenCalledWith({
|
||||
remark: 'existing',
|
||||
strategy: 'leastPing',
|
||||
inboundIds: [2],
|
||||
sortOrder: 3,
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
// A disabled member is dropped by the sub server, so offering it here would
|
||||
// silently stop the balancer document from being emitted (#5645).
|
||||
it('hides disabled inbounds from the member picker', () => {
|
||||
renderModal(null);
|
||||
expect(inboundOptionTitles()).toEqual(['First', 'Second']);
|
||||
});
|
||||
|
||||
it('keeps an already-selected disabled inbound visible when editing', () => {
|
||||
renderModal({
|
||||
id: 8,
|
||||
remark: 'existing',
|
||||
strategy: 'random',
|
||||
inboundIds: [3],
|
||||
sortOrder: 1,
|
||||
enabled: true,
|
||||
});
|
||||
expect(inboundOptionTitles()).toContain('Disabled');
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,11 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, useLocation } from 'react-router';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AllSetting } from '@/models/setting';
|
||||
import SubscriptionGeneralTab from '@/pages/settings/SubscriptionGeneralTab';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
function LocationProbe() {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<output data-testid="location">
|
||||
{location.pathname}
|
||||
{location.hash}
|
||||
</output>
|
||||
);
|
||||
}
|
||||
|
||||
describe('SubscriptionGeneralTab', () => {
|
||||
it('keeps the stored subscription port when the field is cleared', () => {
|
||||
const updateSetting = vi.fn();
|
||||
@@ -53,19 +43,4 @@ describe('SubscriptionGeneralTab', () => {
|
||||
|
||||
expect(updateSetting).toHaveBeenCalledWith({ subPort: 8443 });
|
||||
});
|
||||
|
||||
it('uses router navigation to open subscription format settings', () => {
|
||||
const allSetting = new AllSetting({ subClashEnable: true });
|
||||
|
||||
renderWithProviders(
|
||||
<MemoryRouter initialEntries={['/settings#subscription']}>
|
||||
<SubscriptionGeneralTab allSetting={allSetting} updateSetting={vi.fn()} />
|
||||
<LocationProbe />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open Sub Formats' }));
|
||||
|
||||
expect(screen.getByTestId('location').textContent).toBe('/settings#subscription-formats');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,22 +74,6 @@ describe('useAllSettings', () => {
|
||||
expect(defaults.subURI).toBe('https://example.com/sub/');
|
||||
});
|
||||
|
||||
it('accepts legacy overlength regex settings without logging a response validation warning', async () => {
|
||||
const subJsonUserAgentRegex = 'x'.repeat(2_049);
|
||||
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', { subJsonUserAgentRegex }));
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
const queryClient = makeTestQueryClient();
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAllSettings(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.fetched).toBe(true));
|
||||
expect(result.current.allSetting.subJsonUserAgentRegex).toBe(subJsonUserAgentRegex);
|
||||
expect(warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps an edited setting when a refetch returns older server data', async () => {
|
||||
const values = [{ webPort: 2053 }, { webPort: 2054 }];
|
||||
let index = 0;
|
||||
@@ -164,8 +148,6 @@ describe('useAllSettings', () => {
|
||||
await act(async () => {
|
||||
await result.current.savePayload({
|
||||
...result.current.allSetting,
|
||||
twoFactorEnable: false,
|
||||
twoFactorToken: '',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user