Browse Source

test: add core page test coverage (#11847)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
pull/11855/head
afc163 4 months ago
committed by GitHub
parent
commit
a4d91e49a1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 44
      package-lock.json
  2. 5
      package.json
  3. 64
      src/access.test.ts
  4. 146
      src/app.test.tsx
  5. 240
      src/pages/dashboard/analysis/index.test.tsx
  6. 161
      src/pages/form/basic-form/index.test.tsx
  7. 180
      src/pages/table-list/index.test.tsx
  8. 268
      src/requestErrorConfig.test.ts
  9. 1
      tests/setupTests.ts

44
package-lock.json

@ -33,6 +33,7 @@
"@commitlint/config-conventional": "^21.0.0", "@commitlint/config-conventional": "^21.0.0",
"@tailwindcss/postcss": "^4.3.0", "@tailwindcss/postcss": "^4.3.0",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
@ -65,9 +66,16 @@
"vitest": "4.1.8" "vitest": "4.1.8"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.0.0"
} }
}, },
"node_modules/@adobe/css-tools": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
"integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@ahooksjs/use-request": { "node_modules/@ahooksjs/use-request": {
"version": "2.8.15", "version": "2.8.15",
"resolved": "https://registry.npmjs.org/@ahooksjs/use-request/-/use-request-2.8.15.tgz", "resolved": "https://registry.npmjs.org/@ahooksjs/use-request/-/use-request-2.8.15.tgz",
@ -8184,6 +8192,33 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/react": { "node_modules/@testing-library/react": {
"version": "16.3.2", "version": "16.3.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
@ -15272,6 +15307,13 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
},
"node_modules/cssdb": { "node_modules/cssdb": {
"version": "6.6.3", "version": "6.6.3",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.3.tgz", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.6.3.tgz",

5
package.json

@ -60,6 +60,7 @@
"@commitlint/config-conventional": "^21.0.0", "@commitlint/config-conventional": "^21.0.0",
"@tailwindcss/postcss": "^4.3.0", "@tailwindcss/postcss": "^4.3.0",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
@ -73,7 +74,7 @@
"@umijs/max": "^4.6.57", "@umijs/max": "^4.6.57",
"@umijs/max-plugin-openapi": "^2.0.3", "@umijs/max-plugin-openapi": "^2.0.3",
"@umijs/request-record": "^1.1.4", "@umijs/request-record": "^1.1.4",
"@vitest/coverage-v8": "4.1.8", "@vitest/coverage-v8": "4.1.8",
"@vitest/ui": "4.1.8", "@vitest/ui": "4.1.8",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"express": "^5.2.1", "express": "^5.2.1",
@ -81,7 +82,7 @@
"gh-pages": "^6.3.0", "gh-pages": "^6.3.0",
"happy-dom": "^20.0.0", "happy-dom": "^20.0.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^29.1.1", "jsdom": "^29.1.1",
"lint-staged": "^17.0.2", "lint-staged": "^17.0.2",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"react-doctor": "^0.2.1", "react-doctor": "^0.2.1",

64
src/access.test.ts

@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import access from './access';
describe('access', () => {
it('should return canAdmin true when user has admin access', () => {
const initialState = {
currentUser: {
userid: '1',
name: 'Admin User',
avatar: 'https://example.com/avatar.png',
access: 'admin',
},
};
const result = access(initialState);
expect(result.canAdmin).toBe(true);
});
it('should return canAdmin false when user has non-admin access', () => {
const initialState = {
currentUser: {
userid: '2',
name: 'Regular User',
avatar: 'https://example.com/avatar.png',
access: 'user',
},
};
const result = access(initialState);
expect(result.canAdmin).toBe(false);
});
it('should return canAdmin false when user access is undefined', () => {
const initialState = {
currentUser: {
userid: '3',
name: 'Guest User',
avatar: 'https://example.com/avatar.png',
},
};
const result = access(initialState);
expect(result.canAdmin).toBe(false);
});
it('should return canAdmin false when currentUser is undefined', () => {
const initialState = {
currentUser: undefined,
};
const result = access(initialState);
expect(result.canAdmin).toBeFalsy();
});
it('should return canAdmin false when initialState is undefined', () => {
const result = access(undefined);
expect(result.canAdmin).toBeFalsy();
});
});

146
src/app.test.tsx

@ -0,0 +1,146 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Mock all heavy dependencies before importing app
const mockReplace = vi.fn();
const mockHistory = {
location: {
pathname: '/welcome',
search: '',
hash: '',
},
replace: mockReplace,
};
const mockQueryCurrentUser = vi.fn();
vi.mock('@umijs/max', () => ({
history: mockHistory,
Link: ({ children }: any) => children,
}));
vi.mock('@/services/ant-design-pro/api', () => ({
currentUser: mockQueryCurrentUser,
}));
vi.mock('@/components', () => ({
AvatarDropdown: () => null,
DocLink: () => null,
ErrorBoundary: ({ children }: any) => children,
Footer: () => null,
LangDropdown: () => null,
OfflineBanner: () => null,
VersionDropdown: () => null,
}));
vi.mock('@ant-design/pro-components', () => ({
SettingDrawer: () => null,
}));
vi.mock('@ant-design/icons', () => ({
LinkOutlined: () => null,
}));
vi.mock('./requestErrorConfig', () => ({
errorConfig: {},
}));
vi.mock('../config/defaultSettings', () => ({
default: { navTheme: 'light' },
}));
describe('app getInitialState', () => {
beforeEach(() => {
vi.clearAllMocks();
mockHistory.location = {
pathname: '/welcome',
search: '',
hash: '',
};
});
it('should fetch currentUser when not on login page', async () => {
const { getInitialState } = await import('./app');
mockQueryCurrentUser.mockResolvedValue({
data: {
name: 'Test User',
access: 'admin',
},
});
const state = await getInitialState();
expect(mockQueryCurrentUser).toHaveBeenCalled();
expect(state.currentUser).toEqual({
name: 'Test User',
access: 'admin',
});
expect(state.settingDrawerOpen).toBe(false);
expect(state.fetchUserInfo).toBeDefined();
});
it('should redirect to login when currentUser fetch fails (401)', async () => {
const { getInitialState } = await import('./app');
mockQueryCurrentUser.mockRejectedValue(new Error('401 Unauthorized'));
const state = await getInitialState();
expect(mockReplace).toHaveBeenCalledWith(
expect.stringContaining('/user/login?redirect='),
);
expect(state.currentUser).toBeUndefined();
});
it('should not fetch currentUser on login page', async () => {
const { getInitialState } = await import('./app');
mockHistory.location = {
pathname: '/user/login',
search: '',
hash: '',
};
const state = await getInitialState();
expect(mockQueryCurrentUser).not.toHaveBeenCalled();
expect(state.currentUser).toBeUndefined();
expect(state.fetchUserInfo).toBeDefined();
});
it('should encode redirect path correctly on 401', async () => {
const { getInitialState } = await import('./app');
mockHistory.location = {
pathname: '/admin/users',
search: '?page=2',
hash: '#section',
};
mockQueryCurrentUser.mockRejectedValue(new Error('401'));
await getInitialState();
expect(mockReplace).toHaveBeenCalledWith(
`/user/login?redirect=${encodeURIComponent('/admin/users?page=2#section')}`,
);
});
it('should include default settings in initial state', async () => {
const { getInitialState } = await import('./app');
mockQueryCurrentUser.mockResolvedValue({
data: { name: 'User' },
});
const state = await getInitialState();
expect(state.settings).toEqual({ navTheme: 'light' });
});
it('fetchUserInfo should return user data on success', async () => {
const { getInitialState } = await import('./app');
mockQueryCurrentUser.mockResolvedValue({
data: { name: 'Fetched User', access: 'user' },
});
const state = await getInitialState();
const user = await state.fetchUserInfo?.();
expect(user).toEqual({ name: 'Fetched User', access: 'user' });
});
});

240
src/pages/dashboard/analysis/index.test.tsx

@ -0,0 +1,240 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AnalysisData } from './data.d';
import * as service from './service';
// Mock ProComponents
vi.mock('@ant-design/pro-components', () => ({
GridContent: ({ children }: any) => (
<div data-testid="grid-content">{children}</div>
),
}));
// Mock dependencies
vi.mock('@umijs/max', () => ({
useIntl: () => ({
formatMessage: vi.fn(({ defaultMessage }) => defaultMessage),
}),
FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => (
<span>{defaultMessage}</span>
),
}));
vi.mock('./service', () => ({
fakeChartData: vi.fn(),
}));
vi.mock('./style.style', () => ({
default: () => ({
styles: {
iconGroup: 'icon-group',
currentDate: 'current-date',
},
}),
}));
// Mock child components to avoid heavy rendering
vi.mock('./components/IntroduceRow', () => ({
default: ({ loading, visitData }: any) => (
<div data-testid="introduce-row">
IntroduceRow - {loading ? 'loading' : `${visitData.length} items`}
</div>
),
}));
vi.mock('./components/SalesCard', () => ({
default: ({ loading, salesData }: any) => (
<div data-testid="sales-card">
SalesCard - {loading ? 'loading' : `${salesData.length} items`}
</div>
),
}));
vi.mock('./components/TopSearch', () => ({
default: ({ loading, searchData }: any) => (
<div data-testid="top-search">
TopSearch - {loading ? 'loading' : `${searchData.length} items`}
</div>
),
}));
vi.mock('./components/ProportionSales', () => ({
default: ({ loading, salesPieData }: any) => (
<div data-testid="proportion-sales">
ProportionSales -{' '}
{loading ? 'loading' : `${salesPieData?.length || 0} items`}
</div>
),
}));
vi.mock('./components/OfflineData', () => ({
default: ({ loading, offlineData }: any) => (
<div data-testid="offline-data">
OfflineData - {loading ? 'loading' : `${offlineData.length} items`}
</div>
),
}));
vi.mock('./components/PageLoading', () => ({
default: () => <div data-testid="page-loading">Loading...</div>,
}));
import Analysis from './index';
const mockAnalysisData: AnalysisData = {
visitData: [
{ x: '2024-01-01', y: 100 },
{ x: '2024-01-02', y: 200 },
],
visitData2: [
{ x: '2024-01-01', y: 50 },
{ x: '2024-01-02', y: 150 },
],
salesData: [
{ x: '2024-01', y: 1000 },
{ x: '2024-02', y: 2000 },
],
searchData: [
{ index: 1, keyword: 'test', count: 100, range: 10, status: 0 },
{ index: 2, keyword: 'search', count: 200, range: 20, status: 1 },
],
offlineData: [
{
name: 'Store 1',
cvr: 0.85,
},
{
name: 'Store 2',
cvr: 0.92,
},
],
offlineChartData: [
{ x: '00:00', y1: 10, y2: 20 },
{ x: '01:00', y1: 15, y2: 25 },
],
salesTypeData: [
{ x: 'Category A', y: 1000 },
{ x: 'Category B', y: 2000 },
],
salesTypeDataOnline: [
{ x: 'Online A', y: 500 },
{ x: 'Online B', y: 1500 },
],
salesTypeDataOffline: [
{ x: 'Offline A', y: 500 },
{ x: 'Offline B', y: 500 },
],
radarData: [
{ name: 'Personal', label: 'Quote', value: 10 },
{ name: 'Team', label: 'Quote', value: 30 },
],
};
describe('Analysis Dashboard', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
vi.clearAllMocks();
});
it('should render without crashing', () => {
vi.mocked(service.fakeChartData).mockResolvedValue({
data: mockAnalysisData,
});
const { container } = render(
<QueryClientProvider client={queryClient}>
<Analysis loading={false} dashboardAndanalysis={mockAnalysisData} />
</QueryClientProvider>,
);
expect(container).toBeTruthy();
});
it('should render all dashboard components', async () => {
vi.mocked(service.fakeChartData).mockResolvedValue({
data: mockAnalysisData,
});
render(
<QueryClientProvider client={queryClient}>
<Analysis loading={false} dashboardAndanalysis={mockAnalysisData} />
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('introduce-row')).toBeInTheDocument();
expect(screen.getByTestId('sales-card')).toBeInTheDocument();
expect(screen.getByTestId('top-search')).toBeInTheDocument();
expect(screen.getByTestId('proportion-sales')).toBeInTheDocument();
expect(screen.getByTestId('offline-data')).toBeInTheDocument();
});
});
it('should fetch chart data on mount', async () => {
vi.mocked(service.fakeChartData).mockResolvedValue({
data: mockAnalysisData,
});
render(
<QueryClientProvider client={queryClient}>
<Analysis loading={false} dashboardAndanalysis={mockAnalysisData} />
</QueryClientProvider>,
);
await waitFor(() => {
expect(service.fakeChartData).toHaveBeenCalled();
});
});
it('should pass correct data to child components', async () => {
vi.mocked(service.fakeChartData).mockResolvedValue({
data: mockAnalysisData,
});
render(
<QueryClientProvider client={queryClient}>
<Analysis loading={false} dashboardAndanalysis={mockAnalysisData} />
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('introduce-row')).toHaveTextContent('2 items');
expect(screen.getByTestId('sales-card')).toHaveTextContent('2 items');
});
});
it('should handle empty data gracefully', async () => {
vi.mocked(service.fakeChartData).mockResolvedValue({
data: {
visitData: [],
visitData2: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
},
});
render(
<QueryClientProvider client={queryClient}>
<Analysis loading={false} dashboardAndanalysis={mockAnalysisData} />
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('introduce-row')).toHaveTextContent('0 items');
});
});
});

161
src/pages/form/basic-form/index.test.tsx

@ -0,0 +1,161 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as service from './service';
// Mock ProComponents
vi.mock('@ant-design/pro-components', () => ({
PageContainer: ({ children, content }: any) => (
<div data-testid="page-container">
{content && <div data-testid="page-content">{content}</div>}
{children}
</div>
),
ProForm: ({ children, onFinish }: any) => (
<form
data-testid="pro-form"
onSubmit={(e) => {
e.preventDefault();
onFinish?.();
}}
>
{children}
<button type="submit">提交</button>
</form>
),
ProFormText: ({ label, name }: any) => (
<div data-testid={`form-text-${name}`}>{label}</div>
),
ProFormTextArea: ({ label, name }: any) => (
<div data-testid={`form-textarea-${name}`}>{label}</div>
),
ProFormDateRangePicker: ({ label, name }: any) => (
<div data-testid={`form-daterange-${name}`}>{label}</div>
),
ProFormDigit: ({ label, name }: any) => (
<div data-testid={`form-digit-${name}`}>
{typeof label === 'object' ? label.props.children[0] : label}
</div>
),
ProFormRadio: {
Group: ({ label }: any) => (
<div data-testid="form-radio-group">{label}</div>
),
},
ProFormSelect: ({ name }: any) => (
<div data-testid={`form-select-${name}`}>Select</div>
),
ProFormDependency: ({ children }: any) => (
<div data-testid="form-dependency">{children({ publicType: '2' })}</div>
),
}));
// Mock dependencies
vi.mock('antd', async () => {
const actual = await vi.importActual('antd');
return {
...actual,
message: {
success: vi.fn(),
error: vi.fn(),
},
Card: ({ children }: any) => <div data-testid="card">{children}</div>,
};
});
vi.mock('@umijs/max', () => ({
useIntl: () => ({
formatMessage: vi.fn(({ defaultMessage }) => defaultMessage),
}),
FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => (
<span>{defaultMessage}</span>
),
}));
vi.mock('./service', () => ({
fakeSubmitForm: vi.fn(),
}));
vi.mock('./style.style', () => ({
default: () => ({
styles: {
optional: 'optional-class',
},
}),
}));
import BasicForm from './index';
describe('BasicForm', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
vi.clearAllMocks();
});
it('should render without crashing', () => {
const { container } = render(
<QueryClientProvider client={queryClient}>
<BasicForm />
</QueryClientProvider>,
);
expect(container).toBeTruthy();
});
it('should render form fields', () => {
render(
<QueryClientProvider client={queryClient}>
<BasicForm />
</QueryClientProvider>,
);
expect(screen.getByText('标题')).toBeInTheDocument();
expect(screen.getByText('起止日期')).toBeInTheDocument();
expect(screen.getByText('目标描述')).toBeInTheDocument();
});
it('should have submit button', () => {
render(
<QueryClientProvider client={queryClient}>
<BasicForm />
</QueryClientProvider>,
);
expect(screen.getByRole('button', { name: /提交/i })).toBeInTheDocument();
});
it('should call fakeSubmitForm on successful submission', async () => {
const mockSubmit = vi.mocked(service.fakeSubmitForm);
mockSubmit.mockResolvedValue({ success: true });
render(
<QueryClientProvider client={queryClient}>
<BasicForm />
</QueryClientProvider>,
);
const submitButton = screen.getByRole('button', { name: /提交/i });
submitButton.click();
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalled();
});
});
it('should render within PageContainer', () => {
render(
<QueryClientProvider client={queryClient}>
<BasicForm />
</QueryClientProvider>,
);
expect(screen.getByTestId('page-container')).toBeInTheDocument();
});
});

180
src/pages/table-list/index.test.tsx

@ -0,0 +1,180 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as api from '@/services/ant-design-pro/api';
// Mock ProComponents before importing component
vi.mock('@ant-design/pro-components', () => ({
PageContainer: ({ children }: any) => (
<div data-testid="page-container">{children}</div>
),
ProTable: ({ columns, toolBarRender, request }: any) => {
// Invoke request prop to simulate ProTable data loading
request?.({ current: 1, pageSize: 20 }, {}, {});
return (
<div data-testid="pro-table">
<div data-testid="table-columns">
{columns?.map((col: any) => (
<div
key={
typeof col.title === 'object'
? col.title.props.defaultMessage
: col.dataIndex
}
data-testid={`column-${col.dataIndex}`}
>
{typeof col.title === 'object'
? col.title.props.defaultMessage
: col.title}
</div>
))}
</div>
{toolBarRender && <div data-testid="toolbar">{toolBarRender()}</div>}
</div>
);
},
FooterToolbar: ({ children }: any) => (
<div data-testid="footer-toolbar">{children}</div>
),
ProDescriptions: ({ title }: any) => (
<div data-testid="pro-descriptions">{title}</div>
),
}));
// Mock dependencies
vi.mock('antd', async () => {
const actual = await vi.importActual('antd');
return {
...actual,
message: {
useMessage: () => [
{
success: vi.fn(),
error: vi.fn(),
},
null,
],
success: vi.fn(),
error: vi.fn(),
},
Drawer: ({ children, open }: any) =>
open ? <div data-testid="drawer">{children}</div> : null,
Button: ({ children, onClick }: any) => (
<button type="button" onClick={onClick}>
{children}
</button>
),
Input: (props: any) => <input {...props} />,
};
});
vi.mock('@umijs/max', () => ({
useIntl: () => ({
formatMessage: vi.fn(({ defaultMessage }) => defaultMessage),
}),
FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => (
<span>{defaultMessage}</span>
),
}));
vi.mock('@/services/ant-design-pro/api', () => ({
rule: vi.fn(),
removeRule: vi.fn(),
addRule: vi.fn(),
updateRule: vi.fn(),
}));
vi.mock('./components/CreateForm', () => ({
default: ({ trigger }: any) => <div data-testid="create-form">{trigger}</div>,
}));
vi.mock('./components/UpdateForm', () => ({
default: ({ trigger }: any) => <div data-testid="update-form">{trigger}</div>,
}));
import TableList from './index';
describe('TableList', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
vi.clearAllMocks();
// Mock rule API to return empty data
vi.mocked(api.rule).mockResolvedValue({
data: [],
total: 0,
success: true,
});
});
it('should render without crashing', () => {
const { container } = render(
<QueryClientProvider client={queryClient}>
<TableList />
</QueryClientProvider>,
);
expect(container).toBeTruthy();
});
it('should render ProTable component', () => {
render(
<QueryClientProvider client={queryClient}>
<TableList />
</QueryClientProvider>,
);
expect(screen.getByTestId('pro-table')).toBeInTheDocument();
});
it('should have correct table columns', () => {
render(
<QueryClientProvider client={queryClient}>
<TableList />
</QueryClientProvider>,
);
expect(screen.getByText('Rule name')).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
});
it('should call removeRule mutation successfully', async () => {
const mockRemoveRule = vi.mocked(api.removeRule);
mockRemoveRule.mockResolvedValue({ success: true });
render(
<QueryClientProvider client={queryClient}>
<TableList />
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('pro-table')).toBeInTheDocument();
});
});
it('should handle empty data state', async () => {
vi.mocked(api.rule).mockResolvedValue({
data: [],
total: 0,
success: true,
});
render(
<QueryClientProvider client={queryClient}>
<TableList />
</QueryClientProvider>,
);
await waitFor(() => {
expect(api.rule).toHaveBeenCalled();
});
});
});

268
src/requestErrorConfig.test.ts

@ -0,0 +1,268 @@
import { message, notification } from 'antd';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { errorConfig } from './requestErrorConfig';
vi.mock('antd', () => ({
message: {
warning: vi.fn(),
error: vi.fn(),
},
notification: {
open: vi.fn(),
},
}));
vi.mock('@umijs/max', () => ({
getIntl: vi.fn(() => ({
formatMessage: vi.fn(({ defaultMessage }) => defaultMessage),
})),
}));
describe('requestErrorConfig', () => {
// biome-ignore lint/style/noNonNullAssertion: config handlers are always defined
const errorThrower = errorConfig.errorConfig!.errorThrower!;
// biome-ignore lint/style/noNonNullAssertion: config handlers are always defined
const errorHandler = errorConfig.errorConfig!.errorHandler!;
beforeEach(() => {
vi.clearAllMocks();
});
describe('errorThrower', () => {
it('should throw error when success is false', () => {
const response = {
success: false,
data: null,
errorCode: 400,
errorMessage: 'Bad Request',
showType: 2,
};
expect(() => {
errorThrower(response);
}).toThrow('Bad Request');
});
it('should not throw error when success is true', () => {
const response = {
success: true,
data: { id: 1 },
};
expect(() => {
errorThrower(response);
}).not.toThrow();
});
it('should throw BizError with correct info', () => {
const response = {
success: false,
data: { detail: 'more info' },
errorCode: 403,
errorMessage: 'Forbidden',
showType: 3,
};
expect.assertions(5);
try {
errorThrower(response);
} catch (error: any) {
expect(error.name).toBe('BizError');
expect(error.info.errorCode).toBe(403);
expect(error.info.errorMessage).toBe('Forbidden');
expect(error.info.showType).toBe(3);
expect(error.info.data).toEqual({ detail: 'more info' });
}
});
});
describe('errorHandler', () => {
it('should rethrow error when skipErrorHandler is true', () => {
const error = new Error('Test error');
const opts = { skipErrorHandler: true };
expect(() => {
errorHandler(error, opts);
}).toThrow('Test error');
});
it('should handle SILENT showType', () => {
const error: any = new Error('Silent error');
error.name = 'BizError';
error.info = {
errorCode: 1001,
errorMessage: 'Silent error',
showType: 0,
};
errorHandler(error, {});
expect(message.warning).not.toHaveBeenCalled();
expect(message.error).not.toHaveBeenCalled();
expect(notification.open).not.toHaveBeenCalled();
});
it('should handle WARN_MESSAGE showType', () => {
const error: any = new Error('Warning');
error.name = 'BizError';
error.info = {
errorCode: 1002,
errorMessage: 'This is a warning',
showType: 1,
};
errorHandler(error, {});
expect(message.warning).toHaveBeenCalledWith('This is a warning');
});
it('should handle ERROR_MESSAGE showType', () => {
const error: any = new Error('Error message');
error.name = 'BizError';
error.info = {
errorCode: 1003,
errorMessage: 'This is an error',
showType: 2,
};
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith('This is an error');
});
it('should handle NOTIFICATION showType', () => {
const error: any = new Error('Notification');
error.name = 'BizError';
error.info = {
errorCode: 1004,
errorMessage: 'This is a notification',
showType: 3,
};
errorHandler(error, {});
expect(notification.open).toHaveBeenCalledWith({
title: 1004,
description: 'This is a notification',
});
});
it('should handle REDIRECT showType', () => {
const error: any = new Error('Redirect');
error.name = 'BizError';
error.info = {
errorCode: 401,
errorMessage: 'Unauthorized',
showType: 9,
};
errorHandler(error, {});
// REDIRECT 分支不应触发任何消息/通知提示
expect(message.warning).not.toHaveBeenCalled();
expect(message.error).not.toHaveBeenCalled();
expect(notification.open).not.toHaveBeenCalled();
});
it('should handle default case for unknown showType', () => {
const error: any = new Error('Unknown type');
error.name = 'BizError';
error.info = {
errorCode: 1005,
errorMessage: 'Unknown error type',
showType: 99,
};
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith('Unknown error type');
});
it('should handle axios response error', () => {
const error: any = new Error('Axios error');
error.response = {
status: 500,
data: {},
};
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith('Response status:500');
});
it('should handle offline error', () => {
const error: any = new Error('Network error');
error.request = {};
const originalOnLine = navigator.onLine;
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: false,
});
try {
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith(
'Network unavailable. Please check your connection and try again.',
);
} finally {
Object.defineProperty(navigator, 'onLine', {
writable: true,
value: originalOnLine,
});
}
});
it('should handle request error with no response', () => {
const error: any = new Error('Request error');
error.request = {};
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith(
'None response! Please retry.',
);
});
it('should handle generic error', () => {
const error: any = new Error('Generic error');
errorHandler(error, {});
expect(message.error).toHaveBeenCalledWith(
'Request error, please retry.',
);
});
});
describe('requestInterceptors', () => {
// The interceptor is registered as a plain function (not a tuple),
// so narrow the union type to a callable for the test.
const interceptor = errorConfig.requestInterceptors?.[0] as (config: {
url?: string;
method?: string;
}) => { url?: string };
it('should pass through config without modification', () => {
const config = {
url: 'https://api.example.com/users',
method: 'GET',
};
const result = interceptor(config);
// Token attachment is intentionally commented out in the source;
// interceptor currently returns config as-is
expect(result.url).toBe('https://api.example.com/users');
});
it('should handle URL without config', () => {
const config = {};
const result = interceptor(config);
expect(result.url).toBeUndefined();
});
});
});

1
tests/setupTests.ts

@ -1,3 +1,4 @@
import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest'; import { vi } from 'vitest';
const store: Record<string, string> = {}; const store: Record<string, string> = {};

Loading…
Cancel
Save