mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-13 06:40:59 +00:00
132 lines
5.0 KiB
TypeScript
132 lines
5.0 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { renderHook, waitFor, act } from '@testing-library/react';
|
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useClients } from '@/hooks/useClients';
|
|
import { makeTestQueryClient } from '@/test/test-utils';
|
|
import { HttpUtil, Msg } from '@/utils';
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const emptyPage = {
|
|
items: [],
|
|
total: 0,
|
|
filtered: 0,
|
|
page: 1,
|
|
pageSize: 25,
|
|
groups: [],
|
|
summary: {
|
|
total: 0,
|
|
active: 0,
|
|
onlineCount: 0,
|
|
depletedCount: 0,
|
|
expiringCount: 0,
|
|
deactiveCount: 0,
|
|
online: [],
|
|
depleted: [],
|
|
expiring: [],
|
|
deactive: [],
|
|
},
|
|
};
|
|
|
|
function mockPanel(defaults: Record<string, unknown>) {
|
|
const pagedUrls: string[] = [];
|
|
vi.spyOn(HttpUtil, 'get').mockImplementation(async (url: string) => {
|
|
if (url.includes('/clients/list/paged')) {
|
|
pagedUrls.push(url);
|
|
return new Msg(true, '', emptyPage);
|
|
}
|
|
if (url.includes('/inbounds/options')) return new Msg(true, '', []);
|
|
return new Msg(true, '', null);
|
|
});
|
|
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url: string) => {
|
|
if (url.includes('/setting/defaultSettings')) return new Msg(true, '', defaults);
|
|
if (url.includes('/clients/onlines')) return new Msg(true, '', []);
|
|
return new Msg(true, '', null);
|
|
});
|
|
return pagedUrls;
|
|
}
|
|
|
|
function wrapperFor() {
|
|
const queryClient = makeTestQueryClient();
|
|
return ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
describe('useClients query gating', () => {
|
|
it('does not fetch the list until the page supplies a query', async () => {
|
|
const pagedUrls = mockPanel({ pageSize: 25 });
|
|
const { result } = renderHook(() => useClients(), { wrapper: wrapperFor() });
|
|
|
|
await waitFor(() => expect(result.current.settingsReady).toBe(true));
|
|
// The page has not called setQuery yet, so nothing should have gone out —
|
|
// this is what used to cost a thrown-away round trip on every page load.
|
|
expect(pagedUrls).toEqual([]);
|
|
expect(result.current.fetched).toBe(false);
|
|
});
|
|
|
|
it('issues exactly one request for a page load that settles on one query', async () => {
|
|
const pagedUrls = mockPanel({ pageSize: 50 });
|
|
const { result } = renderHook(() => useClients(), { wrapper: wrapperFor() });
|
|
|
|
await waitFor(() => expect(result.current.settingsReady).toBe(true));
|
|
act(() => {
|
|
result.current.setQuery({ page: 1, pageSize: 50, sort: 'createdAt', order: 'ascend' });
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.fetched).toBe(true));
|
|
expect(pagedUrls).toHaveLength(1);
|
|
expect(pagedUrls[0]).toContain('pageSize=50');
|
|
expect(pagedUrls[0]).toContain('sort=createdAt');
|
|
});
|
|
|
|
it('fetches as soon as a query arrives, without waiting for the settings', async () => {
|
|
// The page remembers the previous visit's page size in localStorage, so on a
|
|
// return visit it can supply a query on the first render. The hook must not
|
|
// hold that back behind /setting/defaultSettings, or the two round trips
|
|
// serialise and the list lands ~160ms later than it needs to.
|
|
const pagedUrls = mockPanel({ pageSize: 25 });
|
|
const { result } = renderHook(() => useClients(), { wrapper: wrapperFor() });
|
|
|
|
act(() => {
|
|
result.current.setQuery({ page: 1, pageSize: 25, sort: 'createdAt', order: 'ascend' });
|
|
});
|
|
await waitFor(() => expect(pagedUrls).toHaveLength(1));
|
|
});
|
|
|
|
it('reports settingsReady even when the settings request fails, so the page can still render', async () => {
|
|
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() });
|
|
|
|
await waitFor(() => expect(result.current.settingsReady).toBe(true));
|
|
});
|
|
|
|
it('skips the list, options and onlines queries for mutation-only callers', async () => {
|
|
const pagedUrls = mockPanel({ pageSize: 25 });
|
|
const postSpy = vi.mocked(HttpUtil.post);
|
|
const { result } = renderHook(() => useClients({ list: false }), { wrapper: wrapperFor() });
|
|
|
|
await waitFor(() => expect(result.current.settingsReady).toBe(true));
|
|
act(() => {
|
|
result.current.setQuery({ page: 1, pageSize: 25, sort: 'createdAt', order: 'ascend' });
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.settingsReady).toBe(true));
|
|
expect(pagedUrls).toEqual([]);
|
|
// subSettings still needs defaultSettings; onlines must not be polled.
|
|
const posted = postSpy.mock.calls.map((c) => String(c[0]));
|
|
expect(posted.some((u) => u.includes('/setting/defaultSettings'))).toBe(true);
|
|
expect(posted.some((u) => u.includes('/clients/onlines'))).toBe(false);
|
|
expect(vi.mocked(HttpUtil.get).mock.calls.map((c) => String(c[0]))).toEqual([]);
|
|
});
|
|
});
|