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:
Muse
2026-10-23 00:39:44 +00:00
109 changed files with 386 additions and 11297 deletions
+3 -1
View File
@@ -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: '',
});
});