feat: overhaul mobile management layouts

This commit is contained in:
Cita
2026-03-21 20:42:25 +08:00
parent 6a19d809f0
commit 09fbf258e2
55 changed files with 4815 additions and 1030 deletions
+169
View File
@@ -0,0 +1,169 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { ToastProvider } from '../components/Toast.js';
import TokenRoutes from './TokenRoutes.js';
const { apiMock, getBrandMock } = vi.hoisted(() => ({
apiMock: {
getRoutesSummary: vi.fn(),
getRouteChannels: vi.fn(),
getModelTokenCandidates: vi.fn(),
getRouteDecisionsBatch: vi.fn(),
getRouteWideDecisionsBatch: vi.fn(),
updateRoute: vi.fn(),
addRoute: vi.fn(),
},
getBrandMock: vi.fn(),
}));
vi.mock('../api.js', () => ({
api: apiMock,
}));
vi.mock('react-dom', async () => {
const actual = await vi.importActual<typeof import('react-dom')>('react-dom');
return {
...actual,
createPortal: (node: unknown) => node,
};
});
vi.mock('../components/useIsMobile.js', () => ({
useIsMobile: () => true,
}));
vi.mock('../components/BrandIcon.js', () => ({
BrandGlyph: ({ brand, icon, model }: { brand?: { name?: string } | null; icon?: string | null; model?: string | null }) => (
<span>{brand?.name || icon || model || ''}</span>
),
InlineBrandIcon: ({ model }: { model: string }) => model ? <span>{model}</span> : null,
getBrand: (...args: unknown[]) => getBrandMock(...args),
hashColor: () => 'linear-gradient(135deg,#4f46e5,#818cf8)',
normalizeBrandIconKey: (icon: string) => icon,
}));
function collectText(node: ReactTestInstance): string {
return (node.children || []).map((child) => {
if (typeof child === 'string') return child;
return collectText(child);
}).join('');
}
function findButtonByText(root: ReactTestInstance, text: string): ReactTestInstance {
return root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& collectText(node).includes(text)
));
}
async function flushMicrotasks() {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}
describe('TokenRoutes mobile actions', () => {
const originalIntersectionObserver = globalThis.IntersectionObserver;
beforeEach(() => {
vi.clearAllMocks();
getBrandMock.mockReset();
getBrandMock.mockReturnValue(null);
globalThis.IntersectionObserver = class {
observe() {}
disconnect() {}
unobserve() {}
} as unknown as typeof IntersectionObserver;
apiMock.getRoutesSummary.mockResolvedValue([
{
id: 1,
modelPattern: 'gpt-4o-mini',
displayName: 'gpt-4o-mini',
displayIcon: null,
modelMapping: null,
routeMode: 'pattern',
routingStrategy: 'weighted',
enabled: true,
channelCount: 1,
enabledChannelCount: 1,
siteNames: ['site-a'],
decisionSnapshot: null,
decisionRefreshedAt: null,
},
]);
apiMock.getRouteChannels.mockResolvedValue([
{
id: 11,
accountId: 101,
tokenId: 1001,
sourceModel: 'gpt-4o-mini',
priority: 0,
weight: 1,
enabled: true,
manualOverride: false,
successCount: 0,
failCount: 0,
account: { username: 'user_a' },
site: { name: 'site-a' },
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
},
]);
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
apiMock.updateRoute.mockResolvedValue({});
apiMock.addRoute.mockResolvedValue({});
});
afterEach(() => {
vi.clearAllMocks();
globalThis.IntersectionObserver = originalIntersectionObserver;
});
it('shows mobile detail expansion and direct management actions', async () => {
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<MemoryRouter initialEntries={['/routes']}>
<ToastProvider>
<TokenRoutes />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
expect(collectText(root!.root)).toContain('筛选');
expect(collectText(root!.root)).toContain('详情');
expect(collectText(root!.root)).toContain('编辑');
expect(collectText(root!.root)).toContain('添加通道');
const disableButton = findButtonByText(root!.root, '禁用');
await act(async () => {
disableButton.props.onClick();
});
await flushMicrotasks();
expect(apiMock.updateRoute).toHaveBeenCalledWith(1, { enabled: false });
const detailButton = findButtonByText(root!.root, '详情');
await act(async () => {
detailButton.props.onClick();
});
await flushMicrotasks();
const text = collectText(root!.root);
expect(text).toContain('user_a');
expect(text).toContain('token-a');
expect(text).toContain('site-a');
} finally {
root?.unmount();
}
});
});