salary-area-select.integration.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
  2. import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  4. import SalaryManagement from '../../src/components/SalaryManagement';
  5. import { salaryClientManager } from '../../src/api/salaryClient';
  6. import { areaClientManager } from '@d8d/area-management-ui/api';
  7. import { AreaSelect } from '@d8d/area-management-ui/components';
  8. // 完整的mock响应对象
  9. const createMockResponse = (status: number, data?: any) => ({
  10. status,
  11. ok: status >= 200 && status < 300,
  12. body: null,
  13. bodyUsed: false,
  14. statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  15. headers: new Headers(),
  16. url: '',
  17. redirected: false,
  18. type: 'basic' as ResponseType,
  19. json: async () => data || {},
  20. text: async () => '',
  21. blob: async () => new Blob(),
  22. arrayBuffer: async () => new ArrayBuffer(0),
  23. formData: async () => new FormData(),
  24. clone: function() { return this; }
  25. });
  26. // 首先取消AreaSelect组件的mock,以便使用真实的组件
  27. vi.doUnmock('@d8d/area-management-ui/components');
  28. // // Mock AreaSelect组件的API
  29. // vi.mock('@d8d/area-management-ui/api', () => ({
  30. // areaClientManager: {
  31. // get: vi.fn(() => ({
  32. // index: {
  33. // $get: vi.fn()
  34. // }
  35. // }))
  36. // }
  37. // }));
  38. // Mock shared-ui-components的hc工具,避免axios调用
  39. vi.mock('@d8d/shared-ui-components/utils/hc', () => ({
  40. axiosFetch: vi.fn(() => Promise.resolve({
  41. status: 200,
  42. json: () => Promise.resolve({ data: [] })
  43. })),
  44. rpcClient: vi.fn(() => ({
  45. $url: vi.fn(),
  46. index: {
  47. $get: vi.fn()
  48. }
  49. }))
  50. }));
  51. // Mock API client
  52. vi.mock('../../src/api/salaryClient', () => {
  53. const mockSalaryClient = {
  54. list: {
  55. $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
  56. data: [
  57. {
  58. id: 1,
  59. provinceId: 110000,
  60. cityId: 110100,
  61. districtId: null,
  62. basicSalary: 5000.00,
  63. allowance: 1000.00,
  64. insurance: 500.00,
  65. housingFund: 800.00,
  66. totalSalary: 4700.00,
  67. updateTime: '2024-01-01T00:00:00Z',
  68. province: { id: 110000, name: '北京市' },
  69. city: { id: 110100, name: '北京市辖区' },
  70. district: null
  71. }
  72. ],
  73. total: 1
  74. })))
  75. },
  76. create: {
  77. $post: vi.fn(() => Promise.resolve(createMockResponse(200, {
  78. id: 3,
  79. provinceId: 440000,
  80. cityId: 440100,
  81. districtId: null,
  82. basicSalary: 5500.00,
  83. allowance: 1100.00,
  84. insurance: 550.00,
  85. housingFund: 850.00,
  86. totalSalary: 5200.00,
  87. updateTime: '2024-01-03T00:00:00Z'
  88. }))),
  89. },
  90. update: {
  91. ':id': {
  92. $put: vi.fn(() => Promise.resolve(createMockResponse(200, {
  93. id: 1,
  94. provinceId: 110000,
  95. cityId: 110100,
  96. districtId: null,
  97. basicSalary: 5500.00,
  98. allowance: 1000.00,
  99. insurance: 500.00,
  100. housingFund: 800.00,
  101. totalSalary: 5200.00,
  102. updateTime: '2024-01-03T00:00:00Z'
  103. }))),
  104. }
  105. },
  106. delete: {
  107. ':id': {
  108. $delete: vi.fn(() => Promise.resolve(createMockResponse(200, { success: true }))),
  109. }
  110. },
  111. detail: {
  112. ':id': {
  113. $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
  114. id: 1,
  115. provinceId: 110000,
  116. cityId: 110100,
  117. districtId: null,
  118. basicSalary: 5000.00,
  119. allowance: 1000.00,
  120. insurance: 500.00,
  121. housingFund: 800.00,
  122. totalSalary: 4700.00,
  123. updateTime: '2024-01-01T00:00:00Z'
  124. }))),
  125. }
  126. },
  127. byProvinceCity: {
  128. $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
  129. id: 1,
  130. provinceId: 110000,
  131. cityId: 110100,
  132. districtId: null,
  133. basicSalary: 5000.00,
  134. allowance: 1000.00,
  135. insurance: 500.00,
  136. housingFund: 800.00,
  137. totalSalary: 4700.00,
  138. updateTime: '2024-01-01T00:00:00Z'
  139. }))),
  140. }
  141. };
  142. const mockClientManager = {
  143. get: vi.fn(() => mockSalaryClient),
  144. reset: vi.fn()
  145. };
  146. return {
  147. salaryClientManager: mockClientManager,
  148. salaryClient: mockSalaryClient
  149. };
  150. });
  151. describe('薪资管理表单中AreaSelect实际行为测试', () => {
  152. let queryClient: QueryClient;
  153. beforeEach(() => {
  154. queryClient = new QueryClient({
  155. defaultOptions: {
  156. queries: {
  157. retry: false,
  158. },
  159. },
  160. });
  161. vi.clearAllMocks();
  162. // 检查AreaSelect是否被mock
  163. console.debug('AreaSelect is mocked?', vi.isMockFunction(AreaSelect));
  164. console.debug('AreaSelect type:', typeof AreaSelect);
  165. // 设置AreaSelect API的mock响应
  166. const mockAreaClient = areaClientManager.get();
  167. // 使用mockImplementation而不是mockImplementationOnce,以便根据查询参数返回不同的数据
  168. (mockAreaClient.index.$get as Mock).mockImplementation(({ query }: any) => {
  169. const filters = query?.filters ? JSON.parse(query.filters) : {};
  170. console.debug('API调用: 查询地区列表, query:', query, 'filters:', filters);
  171. if (filters.level === 1) {
  172. // 省份查询
  173. return Promise.resolve(createMockResponse(200, {
  174. data: [
  175. { id: 110000, name: '北京市', level: 1 },
  176. { id: 310000, name: '上海市', level: 1 },
  177. { id: 440000, name: '广东省', level: 1 }
  178. ]
  179. }));
  180. } else if (filters.level === 2 && filters.parentId === 110000) {
  181. // 北京市的城市查询
  182. return Promise.resolve(createMockResponse(200, {
  183. data: [
  184. { id: 110100, name: '北京市辖区', level: 2 },
  185. { id: 110200, name: '北京市其他', level: 2 }
  186. ]
  187. }));
  188. } else if (filters.level === 3 && filters.parentId === 110100) {
  189. // 北京市辖区的区县查询
  190. return Promise.resolve(createMockResponse(200, {
  191. data: [
  192. { id: 110101, name: '东城区', level: 3 },
  193. { id: 110102, name: '西城区', level: 3 }
  194. ]
  195. }));
  196. } else {
  197. // 默认返回空数组
  198. return Promise.resolve(createMockResponse(200, {
  199. data: []
  200. }));
  201. }
  202. });
  203. });
  204. const renderComponent = () => {
  205. return render(
  206. <QueryClientProvider client={queryClient}>
  207. <SalaryManagement />
  208. </QueryClientProvider>
  209. );
  210. };
  211. it('应该验证添加表单中AreaSelect组件的实际行为 - 区县字段不为必填', async () => {
  212. renderComponent();
  213. // 打开添加模态框
  214. const addButton = screen.getByTestId('add-salary-button');
  215. fireEvent.click(addButton);
  216. await waitFor(() => {
  217. const addSalaryTexts = screen.getAllByText('添加薪资');
  218. expect(addSalaryTexts.length).toBeGreaterThanOrEqual(2);
  219. });
  220. // 辅助函数:获取添加表单中的AreaSelect组件
  221. const getAddFormAreaSelect = () => screen.getByTestId('add-form-area-select');
  222. // 等待AreaSelect组件加载
  223. await waitFor(() => {
  224. const areaSelect = getAddFormAreaSelect();
  225. expect(areaSelect).toBeInTheDocument();
  226. });
  227. // 在添加表单的AreaSelect组件中查找表单标签
  228. const areaSelect1 = getAddFormAreaSelect();
  229. // 直接使用querySelector在AreaSelect内部查找FormLabel元素
  230. const formLabelsInAreaSelect = Array.from(
  231. areaSelect1.querySelectorAll('label[data-slot="form-label"]')
  232. ).filter(label =>
  233. ['省份', '城市', '区县'].some(text => label.textContent?.includes(text))
  234. );
  235. expect(formLabelsInAreaSelect.length).toBe(3); // 省份、城市、区县
  236. // 验证区县标签没有星号(必填标记)
  237. const districtLabel = formLabelsInAreaSelect.find(label =>
  238. label.textContent?.includes('区县')
  239. ) as HTMLElement;
  240. expect(districtLabel).toBeDefined();
  241. // 检查区县标签是否包含星号元素
  242. const districtStarElement = districtLabel.querySelector('span.text-destructive');
  243. expect(districtStarElement).toBeNull(); // 区县标签应该没有星号
  244. // 验证省份标签有星号(因为required=true)
  245. const provinceLabel = formLabelsInAreaSelect.find(label =>
  246. label.textContent?.includes('省份')
  247. ) as HTMLElement;
  248. expect(provinceLabel).toBeDefined();
  249. // 在添加表单中,省份标签应该包含星号
  250. const provinceStarElement = provinceLabel.querySelector('span.text-destructive');
  251. expect(provinceStarElement).not.toBeNull(); // 省份标签应该有星号
  252. // 验证城市标签没有星号(因为还没有选择省份)
  253. const cityLabel = formLabelsInAreaSelect.find(label =>
  254. label.textContent?.includes('城市')
  255. ) as HTMLElement;
  256. expect(cityLabel).toBeDefined();
  257. const cityStarElement = cityLabel.querySelector('span.text-destructive');
  258. expect(cityStarElement).toBeNull(); // 城市标签应该没有星号(未选择省份时)
  259. // 注意:我们不再测试复杂的交互,因为AreaSelect组件的单元测试已经验证了核心逻辑
  260. // 这里我们主要验证在薪资管理上下文中AreaSelect组件被正确使用
  261. });
  262. it('应该验证搜索区域中AreaSelect组件的实际行为 - required=false', async () => {
  263. renderComponent();
  264. // 等待组件渲染
  265. await waitFor(() => {
  266. expect(screen.getByText('薪资水平管理')).toBeInTheDocument();
  267. });
  268. // 查找搜索区域中的AreaSelect组件标签
  269. // 搜索区域的AreaSelect应该使用required=false
  270. const formLabels = screen.getAllByText(/省份|城市|区县/);
  271. // 搜索区域的标签应该没有星号
  272. const searchProvinceLabels = screen.getAllByText('省份');
  273. const searchProvinceLabel = searchProvinceLabels.find(label => {
  274. const element = label as HTMLElement;
  275. const starElement = element.querySelector('span.text-destructive');
  276. return starElement === null;
  277. });
  278. expect(searchProvinceLabel).toBeDefined();
  279. const searchCityLabels = screen.getAllByText('城市');
  280. const searchCityLabel = searchCityLabels.find(label => {
  281. const element = label as HTMLElement;
  282. const starElement = element.querySelector('span.text-destructive');
  283. return starElement === null;
  284. });
  285. expect(searchCityLabel).toBeDefined();
  286. const searchDistrictLabels = screen.getAllByText('区县');
  287. const searchDistrictLabel = searchDistrictLabels.find(label => {
  288. const element = label as HTMLElement;
  289. const starElement = element.querySelector('span.text-destructive');
  290. return starElement === null;
  291. });
  292. expect(searchDistrictLabel).toBeDefined();
  293. });
  294. });