|
|
@@ -0,0 +1,417 @@
|
|
|
+import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
+import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
|
|
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
+import SalaryManagement from '../../src/components/SalaryManagement';
|
|
|
+import { salaryClientManager } from '../../src/api/salaryClient';
|
|
|
+import { areaClientManager } from '@d8d/area-management-ui/api';
|
|
|
+import { AreaSelect } from '@d8d/area-management-ui/components';
|
|
|
+
|
|
|
+
|
|
|
+// 完整的mock响应对象
|
|
|
+const createMockResponse = (status: number, data?: any) => ({
|
|
|
+ status,
|
|
|
+ ok: status >= 200 && status < 300,
|
|
|
+ body: null,
|
|
|
+ bodyUsed: false,
|
|
|
+ statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
|
|
|
+ headers: new Headers(),
|
|
|
+ url: '',
|
|
|
+ redirected: false,
|
|
|
+ type: 'basic' as ResponseType,
|
|
|
+ json: async () => data || {},
|
|
|
+ text: async () => '',
|
|
|
+ blob: async () => new Blob(),
|
|
|
+ arrayBuffer: async () => new ArrayBuffer(0),
|
|
|
+ formData: async () => new FormData(),
|
|
|
+ clone: function() { return this; }
|
|
|
+});
|
|
|
+
|
|
|
+// 首先取消AreaSelect组件的mock,以便使用真实的组件
|
|
|
+vi.doUnmock('@d8d/area-management-ui/components');
|
|
|
+
|
|
|
+// Mock AreaSelect组件的API
|
|
|
+vi.mock('@d8d/area-management-ui/api', () => ({
|
|
|
+ areaClientManager: {
|
|
|
+ get: vi.fn(() => ({
|
|
|
+ index: {
|
|
|
+ $get: vi.fn()
|
|
|
+ }
|
|
|
+ }))
|
|
|
+ }
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock shared-ui-components的hc工具,避免axios调用
|
|
|
+vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
|
|
|
+ axiosFetch: vi.fn(() => Promise.resolve({
|
|
|
+ status: 200,
|
|
|
+ json: () => Promise.resolve({ data: [] })
|
|
|
+ })),
|
|
|
+ rpcClient: vi.fn(() => ({
|
|
|
+ $url: vi.fn(),
|
|
|
+ index: {
|
|
|
+ $get: vi.fn()
|
|
|
+ }
|
|
|
+ }))
|
|
|
+}));
|
|
|
+
|
|
|
+// Mock API client
|
|
|
+vi.mock('../../src/api/salaryClient', () => {
|
|
|
+ const mockSalaryClient = {
|
|
|
+ list: {
|
|
|
+ $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ id: 1,
|
|
|
+ provinceId: 110000,
|
|
|
+ cityId: 110100,
|
|
|
+ districtId: null,
|
|
|
+ basicSalary: 5000.00,
|
|
|
+ allowance: 1000.00,
|
|
|
+ insurance: 500.00,
|
|
|
+ housingFund: 800.00,
|
|
|
+ totalSalary: 4700.00,
|
|
|
+ updateTime: '2024-01-01T00:00:00Z',
|
|
|
+ province: { id: 110000, name: '北京市' },
|
|
|
+ city: { id: 110100, name: '北京市辖区' },
|
|
|
+ district: null
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ total: 1
|
|
|
+ })))
|
|
|
+ },
|
|
|
+ create: {
|
|
|
+ $post: vi.fn(() => Promise.resolve(createMockResponse(200, {
|
|
|
+ id: 3,
|
|
|
+ provinceId: 440000,
|
|
|
+ cityId: 440100,
|
|
|
+ districtId: null,
|
|
|
+ basicSalary: 5500.00,
|
|
|
+ allowance: 1100.00,
|
|
|
+ insurance: 550.00,
|
|
|
+ housingFund: 850.00,
|
|
|
+ totalSalary: 5200.00,
|
|
|
+ updateTime: '2024-01-03T00:00:00Z'
|
|
|
+ }))),
|
|
|
+ },
|
|
|
+ update: {
|
|
|
+ ':id': {
|
|
|
+ $put: vi.fn(() => Promise.resolve(createMockResponse(200, {
|
|
|
+ id: 1,
|
|
|
+ provinceId: 110000,
|
|
|
+ cityId: 110100,
|
|
|
+ districtId: null,
|
|
|
+ basicSalary: 5500.00,
|
|
|
+ allowance: 1000.00,
|
|
|
+ insurance: 500.00,
|
|
|
+ housingFund: 800.00,
|
|
|
+ totalSalary: 5200.00,
|
|
|
+ updateTime: '2024-01-03T00:00:00Z'
|
|
|
+ }))),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ delete: {
|
|
|
+ ':id': {
|
|
|
+ $delete: vi.fn(() => Promise.resolve(createMockResponse(200, { success: true }))),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ detail: {
|
|
|
+ ':id': {
|
|
|
+ $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
|
|
|
+ id: 1,
|
|
|
+ provinceId: 110000,
|
|
|
+ cityId: 110100,
|
|
|
+ districtId: null,
|
|
|
+ basicSalary: 5000.00,
|
|
|
+ allowance: 1000.00,
|
|
|
+ insurance: 500.00,
|
|
|
+ housingFund: 800.00,
|
|
|
+ totalSalary: 4700.00,
|
|
|
+ updateTime: '2024-01-01T00:00:00Z'
|
|
|
+ }))),
|
|
|
+ }
|
|
|
+ },
|
|
|
+ byProvinceCity: {
|
|
|
+ $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
|
|
|
+ id: 1,
|
|
|
+ provinceId: 110000,
|
|
|
+ cityId: 110100,
|
|
|
+ districtId: null,
|
|
|
+ basicSalary: 5000.00,
|
|
|
+ allowance: 1000.00,
|
|
|
+ insurance: 500.00,
|
|
|
+ housingFund: 800.00,
|
|
|
+ totalSalary: 4700.00,
|
|
|
+ updateTime: '2024-01-01T00:00:00Z'
|
|
|
+ }))),
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const mockClientManager = {
|
|
|
+ get: vi.fn(() => mockSalaryClient),
|
|
|
+ reset: vi.fn()
|
|
|
+ };
|
|
|
+
|
|
|
+ return {
|
|
|
+ salaryClientManager: mockClientManager,
|
|
|
+ salaryClient: mockSalaryClient
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+describe('薪资管理表单中AreaSelect实际行为测试', () => {
|
|
|
+ let queryClient: QueryClient;
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ queryClient = new QueryClient({
|
|
|
+ defaultOptions: {
|
|
|
+ queries: {
|
|
|
+ retry: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ vi.clearAllMocks();
|
|
|
+
|
|
|
+ // 检查AreaSelect是否被mock
|
|
|
+ console.debug('AreaSelect is mocked?', vi.isMockFunction(AreaSelect));
|
|
|
+ console.debug('AreaSelect type:', typeof AreaSelect);
|
|
|
+
|
|
|
+ // 设置AreaSelect API的mock响应
|
|
|
+ const mockAreaClient = areaClientManager.get();
|
|
|
+
|
|
|
+ // 省份查询响应 - 更宽松的匹配
|
|
|
+ mockAreaClient.index.$get
|
|
|
+ .mockImplementationOnce(({ query }: any) => {
|
|
|
+ console.debug('API调用 1: 查询省份列表, query:', query);
|
|
|
+ // 总是返回省份数据,不检查查询参数
|
|
|
+ return Promise.resolve(createMockResponse(200, {
|
|
|
+ data: [
|
|
|
+ { id: 110000, name: '北京市', level: 1 },
|
|
|
+ { id: 310000, name: '上海市', level: 1 },
|
|
|
+ { id: 440000, name: '广东省', level: 1 }
|
|
|
+ ]
|
|
|
+ }));
|
|
|
+ })
|
|
|
+ // 城市查询响应(当选择北京市时)
|
|
|
+ .mockImplementationOnce(({ query }: any) => {
|
|
|
+ console.debug('API调用 2: 查询城市列表, query:', query);
|
|
|
+ // 总是返回城市数据
|
|
|
+ return Promise.resolve(createMockResponse(200, {
|
|
|
+ data: [
|
|
|
+ { id: 110100, name: '北京市辖区', level: 2 },
|
|
|
+ { id: 110200, name: '北京市其他', level: 2 }
|
|
|
+ ]
|
|
|
+ }));
|
|
|
+ })
|
|
|
+ // 区县查询响应(当选择北京市辖区时)
|
|
|
+ .mockImplementationOnce(({ query }: any) => {
|
|
|
+ console.debug('API调用 3: 查询区县列表, query:', query);
|
|
|
+ // 总是返回区县数据
|
|
|
+ return Promise.resolve(createMockResponse(200, {
|
|
|
+ data: [
|
|
|
+ { id: 110101, name: '东城区', level: 3 },
|
|
|
+ { id: 110102, name: '西城区', level: 3 }
|
|
|
+ ]
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const renderComponent = () => {
|
|
|
+ return render(
|
|
|
+ <QueryClientProvider client={queryClient}>
|
|
|
+ <SalaryManagement />
|
|
|
+ </QueryClientProvider>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ it('应该验证添加表单中AreaSelect组件的实际行为 - 区县字段不为必填', async () => {
|
|
|
+ renderComponent();
|
|
|
+
|
|
|
+ // 打开添加模态框
|
|
|
+ const addButton = screen.getByTestId('add-salary-button');
|
|
|
+ fireEvent.click(addButton);
|
|
|
+
|
|
|
+ await waitFor(() => {
|
|
|
+ const addSalaryTexts = screen.getAllByText('添加薪资');
|
|
|
+ expect(addSalaryTexts.length).toBeGreaterThanOrEqual(2);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 等待AreaSelect组件加载省份数据
|
|
|
+ await waitFor(() => {
|
|
|
+ // 查找实际的AreaSelect组件中的省份选项
|
|
|
+ console.debug('查找北京市文本...');
|
|
|
+ const beijingElements = screen.getAllByText('北京市');
|
|
|
+ console.debug('找到的北京市元素数量:', beijingElements.length);
|
|
|
+ beijingElements.forEach((el, i) => {
|
|
|
+ console.debug(`北京市元素 ${i}:`, el.outerHTML);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 检查是否有Select组件
|
|
|
+ const selectElements = screen.getAllByRole('combobox');
|
|
|
+ console.debug('找到的combobox元素数量:', selectElements.length);
|
|
|
+ selectElements.forEach((el, i) => {
|
|
|
+ console.debug(`combobox ${i}:`, el.outerHTML);
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(screen.getByText('北京市')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 找到模态框中的AreaSelect组件
|
|
|
+ // 实际的AreaSelect组件会渲染多个FormItem,每个包含FormLabel
|
|
|
+ const formLabels = screen.getAllByText(/省份|城市|区县/);
|
|
|
+ expect(formLabels.length).toBeGreaterThanOrEqual(3);
|
|
|
+
|
|
|
+ // 验证区县标签没有星号(必填标记)
|
|
|
+ // 实际的AreaSelect组件中,区县标签是 <FormLabel>区县</FormLabel>,没有星号
|
|
|
+ const districtLabels = screen.getAllByText('区县');
|
|
|
+ expect(districtLabels.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 检查区县标签是否包含星号元素
|
|
|
+ const districtLabel = districtLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ // 检查是否有包含"text-destructive"类的span元素(星号)
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return !starElement;
|
|
|
+ });
|
|
|
+ expect(districtLabel).toBeDefined();
|
|
|
+
|
|
|
+ // 验证省份标签有星号(因为required=true)
|
|
|
+ const provinceLabels = screen.getAllByText('省份');
|
|
|
+ expect(provinceLabels.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 在添加表单中,省份标签应该包含星号
|
|
|
+ const provinceLabel = provinceLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ // 检查是否有包含"text-destructive"类的span元素(星号)
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return starElement !== null;
|
|
|
+ });
|
|
|
+ expect(provinceLabel).toBeDefined();
|
|
|
+
|
|
|
+ // 选择省份
|
|
|
+ const provinceSelects = screen.getAllByRole('combobox');
|
|
|
+ expect(provinceSelects.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 找到省份选择器并选择北京市
|
|
|
+ const provinceSelect = provinceSelects[0];
|
|
|
+ fireEvent.click(provinceSelect);
|
|
|
+ const beijingOption = screen.getByText('北京市');
|
|
|
+ fireEvent.click(beijingOption);
|
|
|
+
|
|
|
+ // 等待城市数据加载
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByText('北京市辖区')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证城市标签现在有星号(因为选择了省份且required=true)
|
|
|
+ const cityLabels = screen.getAllByText('城市');
|
|
|
+ console.debug('找到的城市标签数量:', cityLabels.length);
|
|
|
+ expect(cityLabels.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 在添加表单中,选择了省份后,城市标签应该包含星号
|
|
|
+ const cityLabel = cityLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ console.debug('检查城市标签HTML:', element.outerHTML);
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ console.debug('找到的星号元素:', starElement);
|
|
|
+ return starElement !== null;
|
|
|
+ });
|
|
|
+ console.debug('找到的带星号的城市标签:', cityLabel);
|
|
|
+ expect(cityLabel).toBeDefined();
|
|
|
+
|
|
|
+ // 选择城市
|
|
|
+ const citySelects = screen.getAllByRole('combobox');
|
|
|
+ const citySelect = citySelects[1]; // 第二个应该是城市选择
|
|
|
+ fireEvent.click(citySelect);
|
|
|
+ const beijingDistrictOption = screen.getByText('北京市辖区');
|
|
|
+ fireEvent.click(beijingDistrictOption);
|
|
|
+
|
|
|
+ // 等待区县数据加载
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByText('东城区')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证区县标签仍然没有星号
|
|
|
+ const updatedDistrictLabels = screen.getAllByText('区县');
|
|
|
+ expect(updatedDistrictLabels.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 区县标签应该没有星号
|
|
|
+ const updatedDistrictLabel = updatedDistrictLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return starElement === null;
|
|
|
+ });
|
|
|
+ expect(updatedDistrictLabel).toBeDefined();
|
|
|
+
|
|
|
+ // 注意:这里我们不选择区县,保持区县为空
|
|
|
+
|
|
|
+ // 填写表单数据
|
|
|
+ const basicSalaryInput = screen.getByLabelText('基本工资');
|
|
|
+ const allowanceInput = screen.getByLabelText('津贴补贴');
|
|
|
+ const insuranceInput = screen.getByLabelText('保险费用');
|
|
|
+ const housingFundInput = screen.getByLabelText('住房公积金');
|
|
|
+
|
|
|
+ fireEvent.change(basicSalaryInput, { target: { value: '5000' } });
|
|
|
+ fireEvent.change(allowanceInput, { target: { value: '1000' } });
|
|
|
+ fireEvent.change(insuranceInput, { target: { value: '500' } });
|
|
|
+ fireEvent.change(housingFundInput, { target: { value: '800' } });
|
|
|
+
|
|
|
+ // 提交表单
|
|
|
+ const submitButton = screen.getByRole('button', { name: /创建薪资/i });
|
|
|
+ fireEvent.click(submitButton);
|
|
|
+
|
|
|
+ // 验证创建API被调用,且districtId为undefined或null
|
|
|
+ await waitFor(() => {
|
|
|
+ const mockClient = salaryClientManager.get();
|
|
|
+ expect(mockClient.create.$post).toHaveBeenCalled();
|
|
|
+
|
|
|
+ // 检查调用参数
|
|
|
+ const callArgs = (mockClient.create.$post as any).mock.calls[0];
|
|
|
+ const requestData = callArgs[0].json;
|
|
|
+
|
|
|
+ // 验证districtId字段不存在或为null(因为区县未选择)
|
|
|
+ expect(requestData.districtId).toBeUndefined();
|
|
|
+
|
|
|
+ // 验证其他必填字段存在
|
|
|
+ expect(requestData.provinceId).toBe(110000);
|
|
|
+ expect(requestData.cityId).toBe(110100);
|
|
|
+ expect(requestData.basicSalary).toBe(5000);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该验证搜索区域中AreaSelect组件的实际行为 - required=false', async () => {
|
|
|
+ renderComponent();
|
|
|
+
|
|
|
+ // 等待组件渲染
|
|
|
+ await waitFor(() => {
|
|
|
+ expect(screen.getByText('薪资水平管理')).toBeInTheDocument();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 查找搜索区域中的AreaSelect组件标签
|
|
|
+ // 搜索区域的AreaSelect应该使用required=false
|
|
|
+ const formLabels = screen.getAllByText(/省份|城市|区县/);
|
|
|
+
|
|
|
+ // 搜索区域的标签应该没有星号
|
|
|
+ const searchProvinceLabels = screen.getAllByText('省份');
|
|
|
+ const searchProvinceLabel = searchProvinceLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return starElement === null;
|
|
|
+ });
|
|
|
+ expect(searchProvinceLabel).toBeDefined();
|
|
|
+
|
|
|
+ const searchCityLabels = screen.getAllByText('城市');
|
|
|
+ const searchCityLabel = searchCityLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return starElement === null;
|
|
|
+ });
|
|
|
+ expect(searchCityLabel).toBeDefined();
|
|
|
+
|
|
|
+ const searchDistrictLabels = screen.getAllByText('区县');
|
|
|
+ const searchDistrictLabel = searchDistrictLabels.find(label => {
|
|
|
+ const element = label as HTMLElement;
|
|
|
+ const starElement = element.querySelector('span.text-destructive');
|
|
|
+ return starElement === null;
|
|
|
+ });
|
|
|
+ expect(searchDistrictLabel).toBeDefined();
|
|
|
+ });
|
|
|
+});
|