Преглед изворни кода

✅ test(salary-management): 新增薪资管理区域选择集成测试

- 新增薪资管理区域选择集成测试文件,验证AreaSelect组件在实际表单中的行为
- 测试添加表单中区县字段不为必填,验证表单提交时districtId字段可为空
- 测试搜索区域中所有字段都不为必填,验证required=false的正确行为
- 改进现有薪资集成测试中的AreaSelect mock,增加区县字段和required参数验证
- 更新测试setup文件,修复ResizeObserver mock以支持Select组件正常工作
yourname пре 2 недеља
родитељ
комит
768b0435fe

+ 417 - 0
allin-packages/salary-management-ui/tests/integration/salary-area-select.integration.test.tsx

@@ -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();
+  });
+});

+ 100 - 28
allin-packages/salary-management-ui/tests/integration/salary.integration.test.tsx

@@ -5,32 +5,78 @@ import SalaryManagement from '../../src/components/SalaryManagement';
 import { salaryClientManager } from '../../src/api/salaryClient';
 // AreaSelect is mocked below
 
-// Mock AreaSelect组件
+// Mock AreaSelect组件 - 改进版本,包含区县字段
 vi.mock('@d8d/area-management-ui/components', () => ({
-  AreaSelect: vi.fn(({ value, onChange, disabled, required: _required }) => (
-    <div data-testid="area-select">
-      <select
-        data-testid="province-select"
-        value={value?.provinceId || ''}
-        onChange={(e) => onChange?.({ ...value, provinceId: e.target.value ? Number(e.target.value) : undefined })}
-        disabled={disabled}
-      >
-        <option value="">选择省份</option>
-        <option value="110000">北京市</option>
-        <option value="310000">上海市</option>
-      </select>
-      <select
-        data-testid="city-select"
-        value={value?.cityId || ''}
-        onChange={(e) => onChange?.({ ...value, cityId: e.target.value ? Number(e.target.value) : undefined })}
-        disabled={disabled || !value?.provinceId}
-      >
-        <option value="">选择城市</option>
-        <option value="110100">北京市辖区</option>
-        <option value="310100">上海市辖区</option>
-      </select>
-    </div>
-  ))
+  AreaSelect: vi.fn(({ value, onChange, disabled, required }) => {
+    // 记录required参数,用于验证
+    console.debug('AreaSelect called with required:', required);
+
+    return (
+      <div data-testid="area-select" data-required={required}>
+        <select
+          data-testid="province-select"
+          value={value?.provinceId || ''}
+          onChange={(e) => {
+            const newValue = {
+              provinceId: e.target.value ? Number(e.target.value) : undefined,
+              cityId: undefined,
+              districtId: undefined
+            };
+            onChange?.(newValue);
+          }}
+          disabled={disabled}
+        >
+          <option value="">选择省份</option>
+          <option value="110000">北京市</option>
+          <option value="310000">上海市</option>
+          <option value="440000">广东省</option>
+        </select>
+        <select
+          data-testid="city-select"
+          value={value?.cityId || ''}
+          onChange={(e) => {
+            const newValue = {
+              provinceId: value?.provinceId,
+              cityId: e.target.value ? Number(e.target.value) : undefined,
+              districtId: undefined
+            };
+            onChange?.(newValue);
+          }}
+          disabled={disabled || !value?.provinceId}
+        >
+          <option value="">选择城市</option>
+          <option value="110100">北京市辖区</option>
+          <option value="310100">上海市辖区</option>
+          <option value="440100">广州市</option>
+        </select>
+        <select
+          data-testid="district-select"
+          value={value?.districtId || ''}
+          onChange={(e) => {
+            const newValue = {
+              provinceId: value?.provinceId,
+              cityId: value?.cityId,
+              districtId: e.target.value ? Number(e.target.value) : undefined
+            };
+            onChange?.(newValue);
+          }}
+          disabled={disabled || !value?.cityId}
+        >
+          <option value="">选择区县</option>
+          <option value="110101">东城区</option>
+          <option value="110102">西城区</option>
+          <option value="310101">黄浦区</option>
+          <option value="440103">荔湾区</option>
+        </select>
+        {/* 显示required参数状态 */}
+        <div data-testid="required-status">
+          省份必填: {required ? '是' : '否'},
+          城市必填: {required && value?.provinceId ? '是' : '否'},
+          区县必填: 否
+        </div>
+      </div>
+    );
+  })
 }));
 
 // 完整的mock响应对象
@@ -248,12 +294,15 @@ describe('薪资管理集成测试', () => {
   it('应该支持区域搜索', async () => {
     renderComponent();
 
+    // 找到搜索区域的AreaSelect组件(第一个)
+    const searchAreaSelect = screen.getAllByTestId('area-select')[0];
+    const provinceSelect = within(searchAreaSelect).getByTestId('province-select');
+    const citySelect = within(searchAreaSelect).getByTestId('city-select');
+
     // 选择省份
-    const provinceSelect = screen.getByTestId('province-select');
     fireEvent.change(provinceSelect, { target: { value: '110000' } });
 
     // 选择城市
-    const citySelect = screen.getByTestId('city-select');
     fireEvent.change(citySelect, { target: { value: '110100' } });
 
     // 点击搜索按钮
@@ -396,12 +445,35 @@ describe('薪资管理集成测试', () => {
 
     // 在模态框中找到AreaSelect组件
     const modalAreaSelect = screen.getAllByTestId('area-select')[1]; // 第二个是模态框中的
+
+    // 验证AreaSelect组件被调用时required=true
+    const areaSelectElement = screen.getAllByTestId('area-select')[1];
+    expect(areaSelectElement).toHaveAttribute('data-required', 'true');
+
+    // 验证区县字段不为必填
+    const requiredStatus = within(modalAreaSelect).getByTestId('required-status');
+    expect(requiredStatus).toHaveTextContent('区县必填: 否');
+
     const provinceSelect = within(modalAreaSelect).getByTestId('province-select');
     const citySelect = within(modalAreaSelect).getByTestId('city-select');
+    const districtSelect = within(modalAreaSelect).getByTestId('district-select');
 
+    // 选择省份(北京市)
     fireEvent.change(provinceSelect, { target: { value: '110000' } });
+
+    // 验证城市字段现在应该是必填的(因为选择了省份且required=true)
+    await waitFor(() => {
+      const updatedRequiredStatus = within(modalAreaSelect).getByTestId('required-status');
+      expect(updatedRequiredStatus).toHaveTextContent('城市必填: 是');
+      expect(updatedRequiredStatus).toHaveTextContent('区县必填: 否');
+    });
+
+    // 选择城市(北京市辖区)
     fireEvent.change(citySelect, { target: { value: '110100' } });
 
+    // 注意:这里我们不选择区县,保持区县为空
+    // 区县字段应该保持为空,且不是必填
+
     // 填写表单数据
     const basicSalaryInput = screen.getByLabelText('基本工资');
     const allowanceInput = screen.getByLabelText('津贴补贴');
@@ -426,7 +498,7 @@ describe('薪资管理集成测试', () => {
       const callArgs = (mockClient.create.$post as any).mock.calls[0];
       const requestData = callArgs[0].json;
 
-      // 验证districtId字段不存在或为null
+      // 验证districtId字段不存在或为null(因为区县未选择)
       expect(requestData.districtId).toBeUndefined();
 
       // 验证其他必填字段存在

+ 15 - 6
allin-packages/salary-management-ui/tests/setup.ts

@@ -16,9 +16,18 @@ Object.defineProperty(window, 'matchMedia', {
   })),
 });
 
-// Mock ResizeObserver
-global.ResizeObserver = vi.fn().mockImplementation(() => ({
-  observe: vi.fn(),
-  unobserve: vi.fn(),
-  disconnect: vi.fn(),
-}));
+// Mock ResizeObserver - 需要调用callback以便Select组件正常工作
+global.ResizeObserver = class ResizeObserver {
+  cb: any;
+  constructor(cb: any) {
+    this.cb = cb;
+  }
+  observe() {
+    this.cb([{ borderBoxSize: { inlineSize: 0, blockSize: 0 } }]);
+  }
+  unobserve() {}
+  disconnect() {}
+};
+
+// Mock Element.scrollIntoView 避免shadcn/ui Select组件错误
+Element.prototype.scrollIntoView = vi.fn();