mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 06:40:59 +00:00
fix(frontend): clean test validation output
This commit is contained in:
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { HttpUtil, Msg } from '@/utils';
|
||||
import { parseMsg } from '@/utils/zodValidate';
|
||||
import { AllSetting } from '@/models/setting';
|
||||
import { AllSettingSchema, type AllSettingInput } from '@/schemas/setting';
|
||||
import { AllSettingResponseSchema, AllSettingSchema, type AllSettingInput } from '@/schemas/setting';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { useServerDraft } from '@/hooks/useServerDraft';
|
||||
|
||||
@@ -17,7 +17,7 @@ type SettingSaveResult = {
|
||||
async function fetchAllSetting(): Promise<AllSettingInput | null> {
|
||||
const msg = await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch settings');
|
||||
const validated = parseMsg(msg, AllSettingSchema, 'setting/all');
|
||||
const validated = parseMsg(msg, AllSettingResponseSchema, 'setting/all');
|
||||
return validated.obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,15 @@ export const AllSettingSchema = z.object({
|
||||
|
||||
export type AllSettingInput = z.infer<typeof AllSettingSchema>;
|
||||
|
||||
// Existing installations can contain regex values saved before the backend
|
||||
// enforced its 2,048-character limit. Accept those values when reading so the
|
||||
// settings page can display and let users correct them, while keeping the
|
||||
// stricter schema above for outgoing updates.
|
||||
export const AllSettingResponseSchema = AllSettingSchema.extend({
|
||||
subJsonUserAgentRegex: z.string().optional(),
|
||||
subClashUserAgentRegex: z.string().optional(),
|
||||
});
|
||||
|
||||
export const FactoryDefaultsSchema = z.record(z.string(), z.string());
|
||||
|
||||
export type FactoryDefaults = z.infer<typeof FactoryDefaultsSchema>;
|
||||
|
||||
@@ -99,7 +99,11 @@ describe('useClients query gating', () => {
|
||||
});
|
||||
|
||||
it('reports settingsReady even when the settings request fails, so the page can still render', async () => {
|
||||
vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', emptyPage));
|
||||
vi.spyOn(HttpUtil, 'get').mockImplementation(async (url: string) => new Msg(
|
||||
true,
|
||||
'',
|
||||
url.includes('/inbounds/options') ? [] : emptyPage,
|
||||
));
|
||||
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(false, 'boom', null));
|
||||
const { result } = renderHook(() => useClients(), { wrapper: wrapperFor() });
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const envelope = (data: unknown): HttpResponse => ({ ok: true, status: 200, stat
|
||||
describe('HttpUtil', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
it('unwraps a success envelope and shows a success toast', async () => {
|
||||
@@ -80,6 +81,7 @@ describe('HttpUtil', () => {
|
||||
|
||||
expect(msg.success).toBe(false);
|
||||
expect(msg.msg).toBe('bad input');
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('maps a thrown native error to a failure Msg via its message', async () => {
|
||||
@@ -88,6 +90,7 @@ describe('HttpUtil', () => {
|
||||
const msg = await HttpUtil.get('/x', undefined, { silent: true });
|
||||
|
||||
expect(msg.msg).toBe('Network down');
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns "No response data" for an empty body', async () => {
|
||||
|
||||
@@ -48,6 +48,16 @@ if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
|
||||
// jsdom does not implement pseudo-element styles or Range geometry. Ant
|
||||
// Design and CodeMirror use these APIs for layout, so supply harmless test
|
||||
// fallbacks instead of emitting noisy "Not implemented" errors.
|
||||
const nativeGetComputedStyle = window.getComputedStyle.bind(window);
|
||||
window.getComputedStyle = ((element: Element) => nativeGetComputedStyle(element)) as typeof window.getComputedStyle;
|
||||
|
||||
if (!Range.prototype.getClientRects) {
|
||||
Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
|
||||
}
|
||||
|
||||
if (!i18next.isInitialized) {
|
||||
void i18next.use(initReactI18next).init({
|
||||
lng: 'en-US',
|
||||
@@ -75,9 +85,12 @@ afterEach(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { HttpUtil, Msg } from '@/utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn(HttpUtil, 'post').mockResolvedValue({ success: true, obj: {} } as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn(HttpUtil, 'get').mockResolvedValue({ success: true, obj: {} } as any);
|
||||
vi.spyOn(HttpUtil, 'get').mockImplementation(async (url: string) => new Msg(
|
||||
true,
|
||||
'',
|
||||
url.includes('/panel/api/inbounds/options') ? [] : {},
|
||||
));
|
||||
|
||||
@@ -13,9 +13,10 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('useAllSettings', () => {
|
||||
it('keeps backend-accepted settings editable when the frontend schema is stricter', async () => {
|
||||
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>
|
||||
@@ -25,6 +26,7 @@ describe('useAllSettings', () => {
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -13,10 +13,15 @@ afterEach(() => {
|
||||
describe('parseMsg', () => {
|
||||
it('rejects a successful response whose payload violates its schema', () => {
|
||||
const msg = new Msg(true, '', { id: 'not-a-number' });
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
expect(() => parseMsg(msg, z.object({ id: z.number() }), 'test/value', { strict: true })).toThrow(
|
||||
'test/value response failed validation',
|
||||
);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[zod] test/value response failed validation',
|
||||
expect.arrayContaining([expect.objectContaining({ code: 'invalid_type', path: ['id'] })]),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves a missing successful payload for callers that handle empty values', () => {
|
||||
@@ -25,17 +30,27 @@ describe('parseMsg', () => {
|
||||
|
||||
it('rejects malformed paged-client payloads', () => {
|
||||
const payload = { items: [], total: 'one', filtered: 1, page: 1, pageSize: 20 };
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
expect(() => parseMsg(new Msg(true, '', payload), ClientPageResponseSchema, 'clients/list/paged', { strict: true })).toThrow(
|
||||
'clients/list/paged response failed validation',
|
||||
);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[zod] clients/list/paged response failed validation',
|
||||
expect.arrayContaining([expect.objectContaining({ code: 'invalid_type', path: ['total'] })]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchXrayConfig', () => {
|
||||
it('keeps a malformed xray payload available for repair', async () => {
|
||||
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', JSON.stringify({ xraySetting: 'not-an-object' })));
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
await expect(fetchXrayConfig()).resolves.toEqual({ xraySetting: 'not-an-object' });
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[zod] xray/ config payload failed validation',
|
||||
expect.arrayContaining([expect.objectContaining({ code: 'invalid_type', path: ['xraySetting'] })]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,11 +75,13 @@ export class HttpUtil {
|
||||
if (!silent) this._handleMsg(msg, silentSuccess);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('GET request failed:', error);
|
||||
const err = error as { response?: { data?: { msg?: string; message?: string } }; message?: string };
|
||||
const data = err.response?.data;
|
||||
const errorMsg = new Msg<T>(false, data?.msg || data?.message || err.message || 'Request failed');
|
||||
if (!silent) this._handleMsg(errorMsg);
|
||||
if (!silent) {
|
||||
console.error('GET request failed:', error);
|
||||
this._handleMsg(errorMsg);
|
||||
}
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
@@ -92,11 +94,13 @@ export class HttpUtil {
|
||||
if (!silent) this._handleMsg(msg, silentSuccess);
|
||||
return msg;
|
||||
} catch (error) {
|
||||
console.error('POST request failed:', error);
|
||||
const err = error as { response?: { data?: { msg?: string; message?: string } }; message?: string };
|
||||
const data = err.response?.data;
|
||||
const errorMsg = new Msg<T>(false, data?.msg || data?.message || err.message || 'Request failed');
|
||||
if (!silent) this._handleMsg(errorMsg);
|
||||
if (!silent) {
|
||||
console.error('POST request failed:', error);
|
||||
this._handleMsg(errorMsg);
|
||||
}
|
||||
return errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||
import { playwright } from '@vitest/browser-playwright';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
||||
const dirname = import.meta.dirname;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
@@ -17,6 +16,8 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: false,
|
||||
// Keep jsdom-heavy form tests within the memory budget of local and CI runners.
|
||||
maxWorkers: 2,
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
|
||||
Reference in New Issue
Block a user