order-person-date-edit.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. import { TIMEOUTS } from '../../utils/timeouts';
  2. import { test, expect } from '../../utils/test-setup';
  3. import type { APIRequestContext } from '@playwright/test';
  4. import { readFileSync } from 'fs';
  5. import { join, dirname } from 'path';
  6. import { fileURLToPath } from 'url';
  7. const __filename = fileURLToPath(import.meta.url);
  8. const __dirname = dirname(__filename);
  9. const testUsers = JSON.parse(readFileSync(join(__dirname, '../../fixtures/test-users.json'), 'utf-8'));
  10. async function getAuthToken(request: APIRequestContext): Promise<string | null> {
  11. const loginResponse = await request.post('http://localhost:8080/api/v1/auth/login', {
  12. data: {
  13. username: testUsers.admin.username,
  14. password: testUsers.admin.password
  15. }
  16. });
  17. if (!loginResponse.ok()) {
  18. console.debug('API 登录失败:', await loginResponse.text());
  19. return null;
  20. }
  21. const loginData = await loginResponse.json();
  22. return loginData.data?.token || loginData.token || null;
  23. }
  24. async function createDisabledPersonViaAPI(
  25. request: APIRequestContext,
  26. personData: {
  27. name: string;
  28. gender: string;
  29. idCard: string;
  30. disabilityId: string;
  31. disabilityType: string;
  32. disabilityLevel: string;
  33. idAddress: string;
  34. phone: string;
  35. province: string;
  36. city: string;
  37. }
  38. ): Promise<{ id: number; name: string } | null> {
  39. try {
  40. const token = await getAuthToken(request);
  41. if (!token) return null;
  42. const createResponse = await request.post('http://localhost:8080/api/v1/disability/createDisabledPerson', {
  43. headers: {
  44. 'Authorization': 'Bearer ' + String(token),
  45. 'Content-Type': 'application/json'
  46. },
  47. data: personData
  48. });
  49. if (!createResponse.ok()) {
  50. const errorText = await createResponse.text();
  51. console.debug('API 创建残疾人失败:', createResponse.status(), errorText);
  52. return null;
  53. }
  54. const result = await createResponse.json();
  55. console.debug('API 创建残疾人成功:', result.name);
  56. return { id: result.id, name: result.name };
  57. } catch (error) {
  58. console.debug('API 调用出错:', error);
  59. return null;
  60. }
  61. }
  62. async function createPlatformViaAPI(
  63. request: APIRequestContext
  64. ): Promise<{ id: number; name: string } | null> {
  65. try {
  66. const token = await getAuthToken(request);
  67. if (!token) return null;
  68. const timestamp = Date.now();
  69. const platformData = {
  70. platformName: '测试平台_' + String(timestamp),
  71. contactPerson: '测试联系人',
  72. contactPhone: '13800138000',
  73. contactEmail: 'test@example.com'
  74. };
  75. const createResponse = await request.post('http://localhost:8080/api/v1/platform/createPlatform', {
  76. headers: {
  77. 'Authorization': 'Bearer ' + String(token),
  78. 'Content-Type': 'application/json'
  79. },
  80. data: platformData
  81. });
  82. if (!createResponse.ok()) {
  83. const errorText = await createResponse.text();
  84. console.debug('API 创建平台失败:', createResponse.status(), errorText);
  85. return null;
  86. }
  87. const result = await createResponse.json();
  88. console.debug('API 创建平台成功:', result.id, result.platformName);
  89. return { id: result.id, name: result.platformName };
  90. } catch (error) {
  91. console.debug('创建平台 API 调用出错:', error);
  92. return null;
  93. }
  94. }
  95. async function createCompanyViaAPI(
  96. request: APIRequestContext,
  97. platformId: number
  98. ): Promise<{ id: number; name: string } | null> {
  99. try {
  100. const token = await getAuthToken(request);
  101. if (!token) return null;
  102. const timestamp = Date.now();
  103. const companyName = '测试公司_' + String(timestamp);
  104. const companyData = {
  105. companyName: companyName,
  106. platformId: platformId,
  107. contactPerson: '测试联系人',
  108. contactPhone: '13900139000',
  109. contactEmail: 'company@example.com'
  110. };
  111. const createResponse = await request.post('http://localhost:8080/api/v1/company/createCompany', {
  112. headers: {
  113. 'Authorization': 'Bearer ' + String(token),
  114. 'Content-Type': 'application/json'
  115. },
  116. data: companyData
  117. });
  118. if (!createResponse.ok()) {
  119. const errorText = await createResponse.text();
  120. console.debug('API 创建公司失败:', createResponse.status(), errorText);
  121. return null;
  122. }
  123. const createResult = await createResponse.json();
  124. if (!createResult.success) {
  125. console.debug('API 创建公司返回 success=false');
  126. return null;
  127. }
  128. const listResponse = await request.get('http://localhost:8080/api/v1/company/getCompaniesByPlatform/' + String(platformId), {
  129. headers: {
  130. 'Authorization': 'Bearer ' + String(token)
  131. }
  132. });
  133. if (!listResponse.ok()) {
  134. console.debug('API 获取公司列表失败');
  135. return null;
  136. }
  137. const companies = await listResponse.json();
  138. const createdCompany = companies.find((c: { companyName: string }) => c.companyName === companyName);
  139. if (createdCompany) {
  140. console.debug('API 创建公司成功:', createdCompany.id, createdCompany.companyName);
  141. return { id: createdCompany.id, name: createdCompany.companyName };
  142. }
  143. console.debug('未找到创建的公司');
  144. return null;
  145. } catch (error) {
  146. console.debug('创建公司 API 调用出错:', error);
  147. return null;
  148. }
  149. }
  150. async function createOrderViaAPI(
  151. request: APIRequestContext,
  152. orderData: {
  153. orderName: string;
  154. platformId: number;
  155. companyId: number;
  156. expectedStartDate: string;
  157. }
  158. ): Promise<{ id: number; name: string } | null> {
  159. try {
  160. const token = await getAuthToken(request);
  161. if (!token) return null;
  162. const createResponse = await request.post('http://localhost:8080/api/v1/order/create', {
  163. headers: {
  164. 'Authorization': 'Bearer ' + String(token),
  165. 'Content-Type': 'application/json'
  166. },
  167. data: orderData
  168. });
  169. if (!createResponse.ok()) {
  170. const errorText = await createResponse.text();
  171. console.debug('API 创建订单失败:', createResponse.status(), errorText);
  172. return null;
  173. }
  174. const result = await createResponse.json();
  175. console.debug('API 创建订单成功:', result.id, result.orderName);
  176. return { id: result.id, name: result.orderName };
  177. } catch (error) {
  178. console.debug('创建订单 API 调用出错:', error);
  179. return null;
  180. }
  181. }
  182. async function bindPersonToOrderViaAPI(
  183. request: APIRequestContext,
  184. orderId: number,
  185. personId: number,
  186. joinDate: string
  187. ): Promise<boolean> {
  188. try {
  189. const token = await getAuthToken(request);
  190. if (!token) return false;
  191. const url = 'http://localhost:8080/api/v1/order/' + String(orderId) + '/persons/batch';
  192. const createResponse = await request.post(url, {
  193. headers: {
  194. 'Authorization': 'Bearer ' + String(token),
  195. 'Content-Type': 'application/json'
  196. },
  197. data: {
  198. persons: [
  199. {
  200. personId: personId,
  201. joinDate: joinDate,
  202. salaryDetail: 5000
  203. }
  204. ]
  205. }
  206. });
  207. if (!createResponse.ok()) {
  208. const errorText = await createResponse.text();
  209. console.debug('API 绑定人员失败:', createResponse.status(), errorText);
  210. return false;
  211. }
  212. const result = await createResponse.json();
  213. if (result.success) {
  214. console.debug('API 绑定人员成功:', personId);
  215. return true;
  216. }
  217. return false;
  218. } catch (error) {
  219. console.debug('绑定人员 API 调用出错:', error);
  220. return false;
  221. }
  222. }
  223. function generateUniqueTestData() {
  224. const timestamp = Date.now();
  225. const counter = Math.floor(Math.random() * 10000);
  226. return {
  227. orderName: '日期测试订单_' + String(timestamp),
  228. personName: '日期测试残疾人_' + String(timestamp),
  229. gender: '男',
  230. idCard: '110101' + String(timestamp).slice(-8) + String(counter).slice(-4),
  231. disabilityId: '残疾证' + String(timestamp).slice(-6) + String(counter),
  232. disabilityType: '视力残疾',
  233. disabilityLevel: '一级',
  234. idAddress: '北京市东城区测试地址' + String(timestamp),
  235. phone: '138' + String(counter).padStart(8, '0'),
  236. province: '北京市',
  237. city: '北京市',
  238. joinDate: '2026-01-15',
  239. newJoinDate: '2026-01-10',
  240. };
  241. }
  242. test.describe.serial('订单管理 - 人员入职/离职日期行内编辑功能 (Story 15.5)', () => {
  243. test.beforeEach(async ({ adminLoginPage }) => {
  244. await adminLoginPage.goto();
  245. await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
  246. });
  247. test('AC1: 订单人员详情页中入职日期和离职日期可点击编辑(行内编辑模式)', async ({ page, request }) => {
  248. console.debug('========== Story 15.5 AC1: 入职/离职日期行内编辑 ==========');
  249. const testData = generateUniqueTestData();
  250. console.debug('测试数据已生成:', testData.personName);
  251. const platform = await createPlatformViaAPI(request);
  252. expect(platform).not.toBeNull();
  253. const company = await createCompanyViaAPI(request, platform!.id);
  254. expect(company).not.toBeNull();
  255. const order = await createOrderViaAPI(request, {
  256. orderName: testData.orderName,
  257. platformId: platform!.id,
  258. companyId: company!.id,
  259. expectedStartDate: '2026-01-01'
  260. });
  261. expect(order).not.toBeNull();
  262. const person = await createDisabledPersonViaAPI(request, {
  263. name: testData.personName,
  264. gender: testData.gender,
  265. idCard: testData.idCard,
  266. disabilityId: testData.disabilityId,
  267. disabilityType: testData.disabilityType,
  268. disabilityLevel: testData.disabilityLevel,
  269. idAddress: testData.idAddress,
  270. phone: testData.phone,
  271. province: testData.province,
  272. city: testData.city,
  273. });
  274. expect(person).not.toBeNull();
  275. const bound = await bindPersonToOrderViaAPI(request, order!.id, person!.id, testData.joinDate);
  276. expect(bound).toBe(true);
  277. await page.goto('/admin/orders');
  278. await page.waitForLoadState('networkidle');
  279. await page.fill('input[placeholder*="搜索"]', testData.orderName);
  280. await page.waitForTimeout(TIMEOUTS.MEDIUM);
  281. const orderRow = page.locator('tbody tr').filter({ hasText: testData.orderName });
  282. await orderRow.waitFor({ state: 'visible', timeout: TIMEOUTS.TABLE_LOAD });
  283. // 点击"打开菜单"按钮
  284. const menuTrigger = orderRow.getByRole('button', { name: /打开菜单/ });
  285. await menuTrigger.click();
  286. await page.waitForTimeout(TIMEOUTS.VERY_SHORT);
  287. // 点击"查看详情"菜单项
  288. const detailButton = page.getByRole('menuitem', { name: /查看详情/ });
  289. await detailButton.click();
  290. await page.waitForSelector('[data-testid="order-detail-dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG });
  291. const joinDateButton = page.locator('[data-testid="edit-join-date-' + String(person!.id) + '"]');
  292. await expect(joinDateButton).toBeVisible();
  293. console.debug('入职日期按钮可见');
  294. const leaveDateButton = page.locator('[data-testid="edit-leave-date-' + String(person!.id) + '"]');
  295. await expect(leaveDateButton).toBeVisible();
  296. console.debug('离职日期按钮可见');
  297. // 点击入职日期按钮,应该弹出日历选择器
  298. await joinDateButton.click();
  299. // 等待日历选择器出现
  300. const calendar = page.locator('[data-slot="calendar"]');
  301. await expect(calendar).toBeVisible({ timeout: TIMEOUTS.DIALOG });
  302. console.debug('日历选择器已打开');
  303. // 验证初始日期被选中
  304. // 验证初始日期被选中 - 查找包含"15"文本的按钮
  305. const selectedDate = calendar.locator('button').filter({ hasText: '15' });
  306. await expect(selectedDate.first()).toBeVisible();
  307. console.debug('初始日期(15日)被正确选中');
  308. console.debug('初始日期(15日)被正确选中');
  309. // 点击新日期(10日)
  310. const newDateButton = calendar.locator('button').filter({ hasText: '10' }).first();
  311. // 增加等待时间确保动画完成
  312. await page.waitForTimeout(TIMEOUTS.SHORT);
  313. // 监听网络请求
  314. let apiCalled = false;
  315. let apiResponse = null;
  316. page.on('response', async (response) => {
  317. if (response.url().includes('/persons/dates')) {
  318. apiCalled = true;
  319. apiResponse = await response.text();
  320. console.debug('API 调用已捕获:', response.status(), apiResponse);
  321. }
  322. });
  323. // 点击新日期
  324. await newDateButton.click({ force: true });
  325. await page.waitForTimeout(TIMEOUTS.MEDIUM);
  326. console.debug('API 是否被调用:', apiCalled);
  327. if (apiCalled) {
  328. console.debug('API 响应:', apiResponse);
  329. }
  330. const toast = page.locator('[data-sonner-toast]');
  331. await expect(toast).toBeVisible();
  332. await expect(toast).toContainText('入职日期更新成功');
  333. console.debug('显示入职日期更新成功 toast');
  334. // 验证日期已更新
  335. await expect(joinDateButton).toContainText(testData.newJoinDate);
  336. console.debug('入职日期已更新为:', testData.newJoinDate);
  337. // 验证日历选择器已关闭
  338. await expect(calendar).not.toBeVisible();
  339. console.debug('日历选择器已自动关闭');
  340. // 测试离职日期编辑
  341. await leaveDateButton.click();
  342. // 等待日历选择器出现
  343. await expect(calendar).toBeVisible({ timeout: TIMEOUTS.DIALOG });
  344. console.debug('离职日期日历选择器已打开');
  345. // 按 ESC 键关闭日历
  346. await page.keyboard.press('Escape');
  347. await expect(calendar).not.toBeVisible();
  348. console.debug('按 ESC 键关闭了日历选择器');
  349. console.debug('========== AC1 测试完成 ==========');
  350. });
  351. });