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 = () => { - +