diff --git a/src/pages/CRM/Graph/index.test.tsx b/src/pages/CRM/Graph/index.test.tsx
index 8454464..dbbdbef 100644
--- a/src/pages/CRM/Graph/index.test.tsx
+++ b/src/pages/CRM/Graph/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,20 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('CRM / Graph page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('shows empty state when graph has no node data', async () => {
+ render();
+
+ await waitFor(
+ () => {
+ expect(screen.getByText(/No relationship data found/)).toBeInTheDocument();
+ },
+ { timeout: 5000 },
+ );
+ });
});
diff --git a/src/pages/CRM/Relations/index.test.tsx b/src/pages/CRM/Relations/index.test.tsx
index d80da87..73fea24 100644
--- a/src/pages/CRM/Relations/index.test.tsx
+++ b/src/pages/CRM/Relations/index.test.tsx
@@ -1,11 +1,58 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
+import { createRelation } from '@/services/api';
describe('CRM / Relations page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders the create relation form', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByPlaceholderText('对方租户ID')).toBeInTheDocument();
+ // antd Form buttons are not exposed in the a11y tree in jsdom; query DOM directly
+ expect(document.querySelector('button[type="submit"]')).toBeInTheDocument();
+ });
+
+ it('renders supplier and customer tabs', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText(/我的供应商/)).toBeInTheDocument();
+ expect(screen.getByText(/我的客户/)).toBeInTheDocument();
+ });
+
+ it('calls createRelation when form is submitted with tenant ID', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const tenantInput = screen.getByPlaceholderText('对方租户ID');
+ await user.type(tenantInput, 'supplier_001');
+
+ const createBtn = document.querySelector('button[type="submit"]') as HTMLElement;
+ await user.click(createBtn);
+
+ await waitFor(() => {
+ expect(vi.mocked(createRelation)).toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/pages/Dashboard/index.test.tsx b/src/pages/Dashboard/index.test.tsx
index 334f5fe..a58a198 100644
--- a/src/pages/Dashboard/index.test.tsx
+++ b/src/pages/Dashboard/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../test/smoke';
import Page from './index';
describe('Dashboard page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('displays stat card titles after data loads', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('产品总数')).toBeInTheDocument();
+ });
+ expect(screen.getByText('总匹数')).toBeInTheDocument();
+ expect(screen.getByText('总盘数')).toBeInTheDocument();
+ });
+
+ it('renders quick navigation section', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('快捷入口')).toBeInTheDocument();
+ });
+ expect(screen.getByText('产品库存')).toBeInTheDocument();
+ expect(screen.getByText('库存查询')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Adjusts/index.test.tsx b/src/pages/Inventory/Adjusts/index.test.tsx
index 788fd50..1df7c08 100644
--- a/src/pages/Inventory/Adjusts/index.test.tsx
+++ b/src/pages/Inventory/Adjusts/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Adjusts page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 创建调整单 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /创建调整单/ })).toBeInTheDocument();
+ });
+
+ it('renders 库存调整 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('库存调整')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Bolts/index.test.tsx b/src/pages/Inventory/Bolts/index.test.tsx
index 984de62..ca5df6b 100644
--- a/src/pages/Inventory/Bolts/index.test.tsx
+++ b/src/pages/Inventory/Bolts/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Bolts page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 匹维度库存 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('匹维度库存')).toBeInTheDocument();
+ });
+
+ it('renders 长度(m) column header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('长度(m)')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Checks/index.test.tsx b/src/pages/Inventory/Checks/index.test.tsx
index 8efd5b0..10fd75a 100644
--- a/src/pages/Inventory/Checks/index.test.tsx
+++ b/src/pages/Inventory/Checks/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Checks page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 创建盘点单 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /创建盘点单/ })).toBeInTheDocument();
+ });
+
+ it('renders 库存盘点 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('库存盘点')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Import/index.test.tsx b/src/pages/Inventory/Import/index.test.tsx
index b5fe27e..90f2378 100644
--- a/src/pages/Inventory/Import/index.test.tsx
+++ b/src/pages/Inventory/Import/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,36 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Import page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 下载导入模板 button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /下载导入模板/ })).toBeInTheDocument();
+ });
+
+ it('renders upload dragger with instructional text', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText(/点击或拖拽文件到此处上传/)).toBeInTheDocument();
+ });
+
+ it('renders import instructions alert', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('导入说明')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Pans/index.test.tsx b/src/pages/Inventory/Pans/index.test.tsx
index 300ff7b..d8500c6 100644
--- a/src/pages/Inventory/Pans/index.test.tsx
+++ b/src/pages/Inventory/Pans/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Pans page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 盘维度库存 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('盘维度库存')).toBeInTheDocument();
+ });
+
+ it('renders 匹数 column header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('匹数')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Stats/index.test.tsx b/src/pages/Inventory/Stats/index.test.tsx
index ccebc4d..c0a0efa 100644
--- a/src/pages/Inventory/Stats/index.test.tsx
+++ b/src/pages/Inventory/Stats/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,26 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Stats page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('displays stat card titles after data loads', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('产品总数')).toBeInTheDocument();
+ });
+ expect(screen.getByText('总匹数')).toBeInTheDocument();
+ expect(screen.getByText('总盘数')).toBeInTheDocument();
+ });
+
+ it('renders color and location breakdown tables', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('按颜色分类统计')).toBeInTheDocument();
+ });
+ expect(screen.getByText('按位置分布统计')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Inventory/Stocks/index.test.tsx b/src/pages/Inventory/Stocks/index.test.tsx
index 5af88c6..6b07b8c 100644
--- a/src/pages/Inventory/Stocks/index.test.tsx
+++ b/src/pages/Inventory/Stocks/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,29 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Inventory / Stocks page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 库存查询 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('库存查询')).toBeInTheDocument();
+ });
+
+ it('renders column headers', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ // These texts appear in both the search form and the table column header
+ expect(screen.getAllByText('产品名称').length).toBeGreaterThan(0);
+ expect(screen.getAllByText('颜色').length).toBeGreaterThan(0);
+ });
});
diff --git a/src/pages/Login/index.test.tsx b/src/pages/Login/index.test.tsx
index 6adc7a4..40f20a4 100644
--- a/src/pages/Login/index.test.tsx
+++ b/src/pages/Login/index.test.tsx
@@ -1,4 +1,6 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +9,46 @@ import { smokeTest } from '../../../test/smoke';
import Page from './index';
describe('Login page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders username and password inputs', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByPlaceholderText('用户名')).toBeInTheDocument();
+ expect(screen.getByPlaceholderText('密码')).toBeInTheDocument();
+ });
+
+ it('renders login submit button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ // antd Form buttons are not exposed in the a11y tree in jsdom; query DOM directly
+ expect(document.querySelector('button[type="submit"]')).toBeInTheDocument();
+ });
+
+ it('shows validation errors when form submitted without values', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const submitBtn = document.querySelector('button[type="submit"]') as HTMLElement;
+ await user.click(submitBtn);
+
+ await waitFor(() => {
+ expect(screen.getByText('请输入用户名')).toBeInTheDocument();
+ });
+ expect(screen.getByText('请输入密码')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Purchase/Orders/detail.test.tsx b/src/pages/Purchase/Orders/detail.test.tsx
index 46d1d2c..a1c35b4 100644
--- a/src/pages/Purchase/Orders/detail.test.tsx
+++ b/src/pages/Purchase/Orders/detail.test.tsx
@@ -1,11 +1,77 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './detail';
+import { getPurchaseOrder } from '@/services/api';
describe('Purchase / Orders / detail page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders order detail sections when order data is available', async () => {
+ vi.mocked(getPurchaseOrder).mockResolvedValueOnce({
+ code: 0,
+ msg: 'ok',
+ success: true,
+ data: {
+ orderId: 'order_001',
+ orderNo: 'PO-2024-001',
+ supplierName: '测试供应商',
+ orderDate: '2024-01-15',
+ purchaseBy: '张三',
+ contractAmount: '10000',
+ receivedQty: '0',
+ paidAmount: '0',
+ receiptStatus: 0,
+ paymentStatus: 0,
+ status: 0,
+ remark: '',
+ details: [],
+ } as any,
+ });
+
+ render();
+
+ await waitFor(() => {
+ // Text is embedded inside "采购订单详情 — PO-2024-001", use regex match
+ expect(screen.getByText(/PO-2024-001/)).toBeInTheDocument();
+ });
+ expect(screen.getByText('测试供应商')).toBeInTheDocument();
+ });
+
+ it('shows 确认订单 button for draft status orders', async () => {
+ vi.mocked(getPurchaseOrder).mockResolvedValueOnce({
+ code: 0,
+ msg: 'ok',
+ success: true,
+ data: {
+ orderId: 'order_001',
+ orderNo: 'PO-2024-001',
+ supplierName: '供应商A',
+ orderDate: '2024-01-15',
+ purchaseBy: '张三',
+ contractAmount: '5000',
+ receivedQty: '0',
+ paidAmount: '0',
+ receiptStatus: 0,
+ paymentStatus: 0,
+ status: 0,
+ remark: '',
+ details: [],
+ } as any,
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /确认订单/ })).toBeInTheDocument();
+ });
+ });
});
diff --git a/src/pages/Purchase/Orders/index.test.tsx b/src/pages/Purchase/Orders/index.test.tsx
index ce1a68a..af1f3ba 100644
--- a/src/pages/Purchase/Orders/index.test.tsx
+++ b/src/pages/Purchase/Orders/index.test.tsx
@@ -1,4 +1,7 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { history } from '@umijs/max';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +10,40 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Purchase / Orders page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 创建采购订单 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /创建采购订单/ })).toBeInTheDocument();
+ });
+
+ it('navigates to new order page on 创建采购订单 click', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /创建采购订单/ }));
+
+ expect(history.push).toHaveBeenCalledWith('/purchase/orders/new');
+ });
+
+ it('renders 采购订单 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('采购订单')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Purchase/Receipts/index.test.tsx b/src/pages/Purchase/Receipts/index.test.tsx
index 46ff523..345ce3f 100644
--- a/src/pages/Purchase/Receipts/index.test.tsx
+++ b/src/pages/Purchase/Receipts/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,28 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Purchase / Receipts page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 入库记录 table header', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('入库记录')).toBeInTheDocument();
+ });
+
+ it('renders status and receipt date column headers', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByText('入库单号')).toBeInTheDocument();
+ expect(screen.getByText('状态')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Purchase/Stats/index.test.tsx b/src/pages/Purchase/Stats/index.test.tsx
index 71c10dc..033a316 100644
--- a/src/pages/Purchase/Stats/index.test.tsx
+++ b/src/pages/Purchase/Stats/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,35 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('Purchase / Stats page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 采购报表 heading', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('采购报表')).toBeInTheDocument();
+ });
+ });
+
+ it('renders summary stat cards', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('采购订单数')).toBeInTheDocument();
+ });
+ // '采购总额' and '已付款' appear in both stat cards and ProTable column headers
+ expect(screen.getAllByText('采购总额').length).toBeGreaterThan(0);
+ expect(screen.getAllByText('已付款').length).toBeGreaterThan(0);
+ expect(screen.getByText('未付款')).toBeInTheDocument();
+ });
+
+ it('renders breakdown table sections', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('按供应商统计')).toBeInTheDocument();
+ });
+ expect(screen.getByText('按产品统计')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Purchase/Suppliers/index.test.tsx b/src/pages/Purchase/Suppliers/index.test.tsx
index 787bdf8..cd4c956 100644
--- a/src/pages/Purchase/Suppliers/index.test.tsx
+++ b/src/pages/Purchase/Suppliers/index.test.tsx
@@ -1,11 +1,82 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
+import { createSupplier } from '@/services/api';
describe('Purchase / Suppliers page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 新增供应商 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /新增供应商/ })).toBeInTheDocument();
+ });
+
+ it('opens create modal on 新增供应商 click', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增供应商/ }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ expect(within(screen.getByRole('dialog')).getByText('新增供应商')).toBeInTheDocument();
+ });
+
+ it('calls createSupplier after filling and submitting form', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增供应商/ }));
+
+ // Wait for the modal and its content to be accessible
+ await waitFor(
+ () => {
+ const dialog = screen.getByRole('dialog');
+ expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1);
+ },
+ { timeout: 5000 },
+ );
+
+ const dialog = screen.getByRole('dialog') as HTMLElement;
+
+ // antd Form.Item + Input is not exposed in the a11y tree; use DOM query
+ await waitFor(() => expect(dialog.querySelector('input')).not.toBeNull(), { timeout: 3000 });
+ const nameInput = dialog.querySelector('input') as HTMLInputElement;
+ fireEvent.change(nameInput, { target: { value: '新供应商' } });
+
+ // The modal footer OK button IS accessible (it is outside the Form element)
+ const buttons = within(dialog).getAllByRole('button');
+ const okBtn = buttons.find((b) => {
+ const text = b.textContent?.replace(/\s/g, '') ?? '';
+ return text.includes('确定') || text === 'OK';
+ });
+ if (okBtn) await user.click(okBtn);
+
+ await waitFor(() => {
+ expect(vi.mocked(createSupplier)).toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/pages/System/Configs/index.test.tsx b/src/pages/System/Configs/index.test.tsx
index 0c6e490..072cdec 100644
--- a/src/pages/System/Configs/index.test.tsx
+++ b/src/pages/System/Configs/index.test.tsx
@@ -1,11 +1,60 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
+import { listConfigs } from '@/services/api';
describe('System / Configs page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders config table column headers', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('配置名称')).toBeInTheDocument();
+ });
+ expect(screen.getByText('配置键')).toBeInTheDocument();
+ expect(screen.getByText('配置值')).toBeInTheDocument();
+ });
+
+ it('shows inline edit input after clicking 编辑', async () => {
+ vi.mocked(listConfigs).mockResolvedValueOnce({
+ code: 0,
+ msg: 'ok',
+ success: true,
+ data: {
+ list: [
+ {
+ configKey: 'site_name',
+ configName: '站点名称',
+ configValue: '木羽系统',
+ configGroup: 'system',
+ remark: '站点显示名称',
+ },
+ ],
+ total: 1,
+ } as any,
+ });
+
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('站点名称')).toBeInTheDocument();
+ });
+
+ await user.click(screen.getByText('编辑'));
+
+ await waitFor(() => {
+ expect(screen.getByDisplayValue('木羽系统')).toBeInTheDocument();
+ });
+ });
});
diff --git a/src/pages/System/Menus/index.test.tsx b/src/pages/System/Menus/index.test.tsx
index 6032924..f8f86a9 100644
--- a/src/pages/System/Menus/index.test.tsx
+++ b/src/pages/System/Menus/index.test.tsx
@@ -1,4 +1,5 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act } from '@testing-library/react';
vi.mock('@umijs/max');
vi.mock('@/services/api');
@@ -7,5 +8,27 @@ import { smokeTest } from '../../../../test/smoke';
import Page from './index';
describe('System / Menus page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 新增菜单 button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /新增菜单/ })).toBeInTheDocument();
+ });
+
+ it('renders menu table column headers', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByText('菜单名称')).toBeInTheDocument();
+ });
+ expect(screen.getByText('路由')).toBeInTheDocument();
+ expect(screen.getByText('类型')).toBeInTheDocument();
+ });
});
diff --git a/src/pages/System/Roles/index.test.tsx b/src/pages/System/Roles/index.test.tsx
index 40ace8d..54626e5 100644
--- a/src/pages/System/Roles/index.test.tsx
+++ b/src/pages/System/Roles/index.test.tsx
@@ -1,11 +1,78 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
+import { createRole } from '@/services/api';
describe('System / Roles page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 新增角色 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /新增角色/ })).toBeInTheDocument();
+ });
+
+ it('opens create dialog on 新增角色 click', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增角色/ }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ expect(within(screen.getByRole('dialog')).getByText('新增角色')).toBeInTheDocument();
+ });
+
+ it('calls createRole after filling required fields and submitting', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增角色/ }));
+
+ // Wait until the modal content is fully accessible (footer buttons appear alongside Close)
+ await waitFor(
+ () => {
+ const dialog = screen.getByRole('dialog');
+ expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1);
+ },
+ { timeout: 5000 },
+ );
+
+ const dialog = screen.getByRole('dialog') as HTMLElement;
+ // Use index-based access: duplicate form IDs from ProTable search break name-based lookup
+ // fireEvent.change is more reliable than user.type for antd controlled ProForm inputs
+ const textboxes = within(dialog).getAllByRole('textbox');
+ fireEvent.change(textboxes[0], { target: { value: '测试角色' } });
+ if (textboxes[1]) fireEvent.change(textboxes[1], { target: { value: 'test_role' } });
+
+ // Click the primary button in the modal footer (the Submit/OK button)
+ const submitBtn = dialog.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement | null;
+ if (submitBtn) fireEvent.click(submitBtn);
+
+ await waitFor(() => {
+ expect(vi.mocked(createRole)).toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/pages/System/Users/index.test.tsx b/src/pages/System/Users/index.test.tsx
index 808784b..0b04d28 100644
--- a/src/pages/System/Users/index.test.tsx
+++ b/src/pages/System/Users/index.test.tsx
@@ -1,11 +1,82 @@
-import { describe, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
vi.mock('@umijs/max');
vi.mock('@/services/api');
import { smokeTest } from '../../../../test/smoke';
import Page from './index';
+import { createUser } from '@/services/api';
describe('System / Users page', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
smokeTest(Page);
+
+ it('renders 新增用户 toolbar button', async () => {
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(screen.getByRole('button', { name: /新增用户/ })).toBeInTheDocument();
+ });
+
+ it('opens create dialog on 新增用户 click', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增用户/ }));
+
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ expect(within(screen.getByRole('dialog')).getByText('新增用户')).toBeInTheDocument();
+ });
+
+ it('calls createUser after filling required fields and submitting', async () => {
+ const user = userEvent.setup();
+ render();
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ await user.click(screen.getByRole('button', { name: /新增用户/ }));
+
+ // Wait until modal content is fully accessible (footer buttons appear beyond Close)
+ await waitFor(
+ () => {
+ const dialog = screen.getByRole('dialog');
+ expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1);
+ },
+ { timeout: 5000 },
+ );
+
+ const dialog = screen.getByRole('dialog') as HTMLElement;
+ // Use index-based access; duplicate form IDs from ProTable search break name-based lookup
+ // fireEvent.change is more reliable for antd controlled ProForm inputs
+ const textboxes = within(dialog).getAllByRole('textbox');
+ fireEvent.change(textboxes[0], { target: { value: 'testuser' } });
+ if (textboxes[1]) fireEvent.change(textboxes[1], { target: { value: '测试用户' } });
+
+ // Fill password field (type=password) using DOM query
+ const passwordInput = dialog.querySelector('input[type="password"]') as HTMLInputElement | null;
+ if (passwordInput) fireEvent.change(passwordInput, { target: { value: 'Password123' } });
+
+ // Click the primary button in the modal footer (Submit/OK)
+ const submitBtn = dialog.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement | null;
+ if (submitBtn) fireEvent.click(submitBtn);
+
+ await waitFor(() => {
+ expect(vi.mocked(createUser)).toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/pages/System/Users/index.tsx b/src/pages/System/Users/index.tsx
index 1deb5a3..5cd9fbc 100644
--- a/src/pages/System/Users/index.tsx
+++ b/src/pages/System/Users/index.tsx
@@ -100,7 +100,7 @@ const UsersPage: React.FC = () => {
-
+