salary-selector.integration.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
  2. import { render, screen, fireEvent, waitFor } from '@testing-library/react';
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  4. import SalarySelector from '../../src/components/SalarySelector';
  5. import { salaryClientManager } from '../../src/api/salaryClient';
  6. // AreaSelect is mocked below
  7. // Mock AreaSelect组件
  8. vi.mock('@d8d/area-management-ui/components', () => ({
  9. AreaSelect: vi.fn(({ value, onChange, disabled, required }) => (
  10. <div data-testid="area-select">
  11. <label>
  12. {required ? '选择区域*' : '选择区域'}
  13. <select
  14. data-testid="province-select"
  15. value={value?.provinceId || ''}
  16. onChange={(e) => onChange?.({ ...value, provinceId: e.target.value ? Number(e.target.value) : undefined })}
  17. disabled={disabled}
  18. >
  19. <option value="">选择省份</option>
  20. <option value="110000">北京市</option>
  21. <option value="310000">上海市</option>
  22. <option value="440000">广东省</option>
  23. </select>
  24. <select
  25. data-testid="city-select"
  26. value={value?.cityId || ''}
  27. onChange={(e) => onChange?.({ ...value, cityId: e.target.value ? Number(e.target.value) : undefined })}
  28. disabled={disabled || !value?.provinceId}
  29. >
  30. <option value="">选择城市</option>
  31. <option value="110100">北京市辖区</option>
  32. <option value="310100">上海市辖区</option>
  33. <option value="440100">广州市</option>
  34. </select>
  35. </label>
  36. </div>
  37. ))
  38. }));
  39. // 完整的mock响应对象
  40. const createMockResponse = (status: number, data?: any) => ({
  41. status,
  42. ok: status >= 200 && status < 300,
  43. body: null,
  44. bodyUsed: false,
  45. statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  46. headers: new Headers(),
  47. url: '',
  48. redirected: false,
  49. type: 'basic' as ResponseType,
  50. json: async () => data || {},
  51. text: async () => '',
  52. blob: async () => new Blob(),
  53. arrayBuffer: async () => new ArrayBuffer(0),
  54. formData: async () => new FormData(),
  55. clone: function() { return this; }
  56. });
  57. // Mock API client
  58. vi.mock('../../src/api/salaryClient', () => {
  59. const mockSalaryClient = {
  60. byProvinceCity: {
  61. $get: vi.fn()
  62. }
  63. };
  64. const mockClientManager = {
  65. get: vi.fn(() => mockSalaryClient),
  66. reset: vi.fn()
  67. };
  68. return {
  69. salaryClientManager: mockClientManager,
  70. salaryClient: mockSalaryClient
  71. };
  72. });
  73. describe('薪资选择器集成测试', () => {
  74. let queryClient: QueryClient;
  75. let mockOnChange: Mock;
  76. let mockSalaryClient: any;
  77. beforeEach(() => {
  78. queryClient = new QueryClient({
  79. defaultOptions: {
  80. queries: {
  81. retry: false,
  82. },
  83. },
  84. });
  85. mockOnChange = vi.fn();
  86. // 从模拟中获取mock客户端
  87. mockSalaryClient = salaryClientManager.get();
  88. vi.clearAllMocks();
  89. });
  90. const renderComponent = (props = {}) => {
  91. return render(
  92. <QueryClientProvider client={queryClient}>
  93. <SalarySelector onChange={mockOnChange} {...props} />
  94. </QueryClientProvider>
  95. );
  96. };
  97. it('应该正确渲染薪资选择器组件', () => {
  98. renderComponent();
  99. // 检查区域选择器 - 使用更精确的选择器,因为现在有两个"选择区域"文本
  100. expect(screen.getAllByText('选择区域').length).toBeGreaterThanOrEqual(1);
  101. expect(screen.getByTestId('area-select')).toBeInTheDocument();
  102. // 检查初始状态提示
  103. expect(screen.getByText('请先选择省份和城市以查询薪资信息')).toBeInTheDocument();
  104. });
  105. it('应该选择区域后查询薪资', async () => {
  106. // Mock成功的薪资查询
  107. mockSalaryClient.byProvinceCity.$get.mockResolvedValueOnce(
  108. createMockResponse(200, {
  109. id: 1,
  110. provinceId: 110000,
  111. cityId: 110100,
  112. districtId: null,
  113. basicSalary: 5000.00,
  114. allowance: 1000.00,
  115. insurance: 500.00,
  116. housingFund: 800.00,
  117. totalSalary: 4700.00,
  118. updateTime: '2024-01-01T00:00:00Z'
  119. })
  120. );
  121. renderComponent();
  122. // 选择省份
  123. const provinceSelect = screen.getByTestId('province-select');
  124. fireEvent.change(provinceSelect, { target: { value: '110000' } });
  125. // 选择城市
  126. const citySelect = screen.getByTestId('city-select');
  127. fireEvent.change(citySelect, { target: { value: '110100' } });
  128. // 验证API调用
  129. await waitFor(() => {
  130. expect(mockSalaryClient.byProvinceCity.$get).toHaveBeenCalledWith({
  131. query: {
  132. provinceId: 110000,
  133. cityId: 110100
  134. }
  135. });
  136. });
  137. // 验证onChange被调用
  138. expect(mockOnChange).toHaveBeenCalledWith({
  139. provinceId: 110000,
  140. cityId: 110100,
  141. salary: undefined,
  142. salaryDetail: undefined
  143. });
  144. // 等待薪资信息显示
  145. await waitFor(() => {
  146. expect(screen.getByText('薪资信息')).toBeInTheDocument();
  147. expect(screen.getByText('自动查询薪资模式')).toBeInTheDocument();
  148. expect(screen.getByText('基本工资')).toBeInTheDocument();
  149. expect(screen.getByText('¥5000.00')).toBeInTheDocument();
  150. expect(screen.getByText('总薪资')).toBeInTheDocument();
  151. expect(screen.getByText('¥4700.00')).toBeInTheDocument();
  152. });
  153. // 验证最终的onChange调用(包含薪资详情)
  154. await waitFor(() => {
  155. expect(mockOnChange).toHaveBeenCalledWith({
  156. provinceId: 110000,
  157. cityId: 110100,
  158. salary: 4700.00,
  159. salaryDetail: expect.objectContaining({
  160. basicSalary: 5000.00,
  161. allowance: 1000.00,
  162. insurance: 500.00,
  163. housingFund: 800.00
  164. })
  165. });
  166. });
  167. });
  168. it('应该处理区域无薪资记录的情况', async () => {
  169. // Mock 404响应(无薪资记录)
  170. mockSalaryClient.byProvinceCity.$get.mockResolvedValueOnce(
  171. createMockResponse(404, { code: 404, message: '该省份城市的薪资记录不存在' })
  172. );
  173. renderComponent({ allowManualAdjust: true });
  174. // 选择区域
  175. const provinceSelect = screen.getByTestId('province-select');
  176. fireEvent.change(provinceSelect, { target: { value: '440000' } });
  177. const citySelect = screen.getByTestId('city-select');
  178. fireEvent.change(citySelect, { target: { value: '440100' } });
  179. // 检查无薪资记录提示
  180. await waitFor(() => {
  181. expect(screen.getByText('该区域暂无薪资记录')).toBeInTheDocument();
  182. expect(screen.getByText('您可以切换到手动模式输入薪资,或联系管理员添加该区域的薪资标准')).toBeInTheDocument();
  183. });
  184. });
  185. it('应该支持手动调整薪资模式', async () => {
  186. // Mock成功的薪资查询
  187. mockSalaryClient.byProvinceCity.$get.mockResolvedValueOnce(
  188. createMockResponse(200, {
  189. id: 1,
  190. provinceId: 110000,
  191. cityId: 110100,
  192. basicSalary: 5000.00,
  193. allowance: 1000.00,
  194. insurance: 500.00,
  195. housingFund: 800.00,
  196. totalSalary: 4700.00
  197. })
  198. );
  199. renderComponent({ allowManualAdjust: true });
  200. // 选择区域
  201. const provinceSelect = screen.getByTestId('province-select');
  202. fireEvent.change(provinceSelect, { target: { value: '110000' } });
  203. const citySelect = screen.getByTestId('city-select');
  204. fireEvent.change(citySelect, { target: { value: '110100' } });
  205. // 等待自动查询完成并显示切换按钮
  206. await waitFor(() => {
  207. expect(screen.getByText('自动查询薪资模式')).toBeInTheDocument();
  208. expect(screen.getByText('切换到手动调整模式')).toBeInTheDocument();
  209. });
  210. // 切换到手动模式
  211. const toggleButton = screen.getByText('切换到手动调整模式');
  212. fireEvent.click(toggleButton);
  213. // 检查手动模式
  214. await waitFor(() => {
  215. expect(screen.getByText('手动调整薪资模式')).toBeInTheDocument();
  216. expect(screen.getByLabelText('手动输入薪资')).toBeInTheDocument();
  217. expect(screen.getByDisplayValue('4700')).toBeInTheDocument(); // 自动查询的总薪资
  218. });
  219. // 修改手动薪资
  220. const salaryInput = screen.getByLabelText('手动输入薪资');
  221. fireEvent.change(salaryInput, { target: { value: '6000' } });
  222. // 验证onChange被调用
  223. expect(mockOnChange).toHaveBeenCalledWith({
  224. provinceId: 110000,
  225. cityId: 110100,
  226. salary: 6000,
  227. salaryDetail: undefined
  228. });
  229. // 检查总薪资显示(等待状态更新)
  230. await waitFor(() => {
  231. // 查找包含"6000.00"的文本
  232. expect(screen.getByText(/6000\.00/)).toBeInTheDocument();
  233. });
  234. // 切换回自动模式
  235. fireEvent.click(screen.getByText('切换回自动查询模式'));
  236. // 验证重新查询
  237. await waitFor(() => {
  238. expect(mockSalaryClient.byProvinceCity.$get).toHaveBeenCalledTimes(2); // 初始查询 + 重新查询
  239. });
  240. });
  241. it('应该禁用手动调整时隐藏切换按钮', async () => {
  242. // Mock成功的薪资查询
  243. mockSalaryClient.byProvinceCity.$get.mockResolvedValueOnce(
  244. createMockResponse(200, {
  245. id: 1,
  246. provinceId: 110000,
  247. cityId: 110100,
  248. basicSalary: 5000.00,
  249. allowance: 1000.00,
  250. insurance: 500.00,
  251. housingFund: 800.00,
  252. totalSalary: 4700.00
  253. })
  254. );
  255. renderComponent({ allowManualAdjust: false });
  256. // 选择区域
  257. const provinceSelect = screen.getByTestId('province-select');
  258. fireEvent.change(provinceSelect, { target: { value: '110000' } });
  259. const citySelect = screen.getByTestId('city-select');
  260. fireEvent.change(citySelect, { target: { value: '110100' } });
  261. // 等待自动查询完成
  262. await waitFor(() => {
  263. expect(screen.getByText('自动查询薪资模式')).toBeInTheDocument();
  264. });
  265. // 检查切换按钮不存在
  266. expect(screen.queryByText('切换到手动调整模式')).not.toBeInTheDocument();
  267. });
  268. it('应该处理API查询错误', async () => {
  269. // Mock API错误
  270. mockSalaryClient.byProvinceCity.$get.mockRejectedValueOnce(new Error('网络错误'));
  271. renderComponent();
  272. // 选择区域
  273. const provinceSelect = screen.getByTestId('province-select');
  274. fireEvent.change(provinceSelect, { target: { value: '110000' } });
  275. const citySelect = screen.getByTestId('city-select');
  276. fireEvent.change(citySelect, { target: { value: '110100' } });
  277. // 检查错误提示
  278. await waitFor(() => {
  279. expect(screen.getByText('查询薪资信息失败,请检查网络连接或稍后重试')).toBeInTheDocument();
  280. });
  281. });
  282. it('应该支持外部值控制', async () => {
  283. const initialValue = {
  284. provinceId: 110000,
  285. cityId: 110100,
  286. salary: 5000,
  287. salaryDetail: {
  288. id: 1,
  289. provinceId: 110000,
  290. cityId: 110100,
  291. basicSalary: 5000.00,
  292. allowance: 1000.00,
  293. insurance: 500.00,
  294. housingFund: 800.00,
  295. totalSalary: 4700.00
  296. } as any
  297. };
  298. // Mock API调用,返回与外部值相同的数据
  299. mockSalaryClient.byProvinceCity.$get.mockResolvedValueOnce(
  300. createMockResponse(200, initialValue.salaryDetail)
  301. );
  302. renderComponent({ value: initialValue });
  303. // 检查区域选择器已设置值
  304. const provinceSelect = screen.getByTestId('province-select') as HTMLSelectElement;
  305. const citySelect = screen.getByTestId('city-select') as HTMLSelectElement;
  306. expect(provinceSelect.value).toBe('110000');
  307. expect(citySelect.value).toBe('110100');
  308. // 检查薪资信息显示
  309. await waitFor(() => {
  310. expect(screen.getByText('薪资信息')).toBeInTheDocument();
  311. expect(screen.getByText('¥5000.00')).toBeInTheDocument(); // 基本工资
  312. expect(screen.getByText('¥4700.00')).toBeInTheDocument(); // 总薪资
  313. });
  314. });
  315. it('应该支持禁用状态', () => {
  316. renderComponent({ disabled: true });
  317. // 检查区域选择器被禁用
  318. const provinceSelect = screen.getByTestId('province-select');
  319. expect(provinceSelect).toBeDisabled();
  320. });
  321. it('应该支持必填验证', () => {
  322. renderComponent({ required: true });
  323. // 检查必填标记
  324. expect(screen.getByText('选择区域*')).toBeInTheDocument();
  325. });
  326. });