status-update-sync.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. import { TIMEOUTS } from '../../utils/timeouts';
  2. import { test } from '../../utils/test-setup';
  3. import { AdminLoginPage } from '../../pages/admin/login.page';
  4. import { OrderManagementPage, WORK_STATUS, WORK_STATUS_LABELS, type WorkStatus } from '../../pages/admin/order-management.page';
  5. import { EnterpriseMiniPage } from '../../pages/mini/enterprise-mini.page';
  6. import { TalentMiniPage } from '../../pages/mini/talent-mini.page';
  7. /**
  8. * 跨端数据同步 E2E 测试 - 人员状态更新
  9. *
  10. * 测试目标:验证后台更新人员工作状态后,企业小程序和人才小程序能否正确显示更新后的状态
  11. *
  12. * 测试流程:
  13. * 1. 后台操作:登录 → 导航到订单管理 → 打开订单详情 → 更新人员工作状态 → 验证后台更新成功
  14. * 2. 企业小程序验证:登录 → 导航到订单详情 → 验证人员状态同步正确
  15. * 3. 人才小程序验证:登录 → 导航到订单详情 → 验证人员状态同步正确
  16. *
  17. * 测试要点:
  18. * - 使用两个独立的 browser context(后台和小程序)
  19. * - 记录数据同步时间
  20. * - 验证工作状态、入职日期、离职日期字段完整性
  21. * - 测试多种工作状态流转
  22. * - 使用 data-testid 选择器
  23. * - 测试数据清理策略
  24. */
  25. // 测试常量
  26. const TEST_SYNC_TIMEOUT = 5000; // 数据同步等待时间(ms),基于实际测试调整
  27. // 企业小程序登录凭证
  28. const ENTERPRISE_MINI_LOGIN_PHONE = '13800001111';
  29. const ENTERPRISE_MINI_LOGIN_PASSWORD = process.env.TEST_ENTERPRISE_PASSWORD || 'password123';
  30. // 人才小程序登录凭证
  31. const TALENT_MINI_LOGIN_PHONE = '13900001111';
  32. const TALENT_MINI_LOGIN_PASSWORD = process.env.TEST_TALENT_PASSWORD || 'password123';
  33. /**
  34. * 后台登录辅助函数
  35. */
  36. async function loginAdmin(page: any, testUsers: any) {
  37. const adminLoginPage = new AdminLoginPage(page);
  38. await adminLoginPage.goto();
  39. await adminLoginPage.page.getByPlaceholder('请输入用户名').fill(testUsers.admin.username);
  40. await adminLoginPage.page.getByPlaceholder('请输入密码').fill(testUsers.admin.password);
  41. await adminLoginPage.page.getByRole('button', { name: '登录' }).click();
  42. // 使用更宽松的等待逻辑 - 不强制要求 dashboard
  43. await adminLoginPage.page.waitForTimeout(TIMEOUTS.LONG);
  44. const currentUrl = adminLoginPage.page.url();
  45. if (currentUrl.includes('/admin/dashboard') || currentUrl.includes('/admin/')) {
  46. console.debug('[后台] 登录成功');
  47. } else {
  48. console.debug(`[后台] 登录后 URL: ${currentUrl}`);
  49. }
  50. }
  51. /**
  52. * 企业小程序登录辅助函数
  53. */
  54. async function loginEnterpriseMini(page: any) {
  55. const miniPage = new EnterpriseMiniPage(page);
  56. await miniPage.goto();
  57. await miniPage.login(ENTERPRISE_MINI_LOGIN_PHONE, ENTERPRISE_MINI_LOGIN_PASSWORD);
  58. await miniPage.expectLoginSuccess();
  59. console.debug('[企业小程序] 登录成功');
  60. }
  61. /**
  62. * 人才小程序登录辅助函数
  63. */
  64. async function loginTalentMini(page: any) {
  65. const miniPage = new TalentMiniPage(page);
  66. await miniPage.goto();
  67. await miniPage.login(TALENT_MINI_LOGIN_PHONE, TALENT_MINI_LOGIN_PASSWORD);
  68. await miniPage.expectLoginSuccess();
  69. console.debug('[人才小程序] 登录成功');
  70. }
  71. /**
  72. * 遍历订单列表,找到第一个有关联人员的订单
  73. * @param page - Playwright Page 对象
  74. * @param orderPage - OrderManagementPage 实例
  75. * @returns 订单名称和第一个人员名称
  76. */
  77. async function findFirstOrderWithPersons(page: any, orderPage: OrderManagementPage): Promise<{ orderName: string; personName: string }> {
  78. // 获取所有订单行
  79. const allOrderRows = await page.locator('table tbody tr').all();
  80. console.debug(`[订单查找] 总共 ${allOrderRows.length} 个订单,开始查找有人员的订单`);
  81. for (let i = 0; i < allOrderRows.length; i++) {
  82. const orderRow = allOrderRows[i];
  83. const orderNameCell = orderRow.locator('td').first();
  84. const orderName = await orderNameCell.textContent();
  85. if (!orderName) continue;
  86. const trimmedOrderName = orderName.trim();
  87. console.debug(`[订单查找] 检查第 ${i + 1} 个订单: ${trimmedOrderName}`);
  88. try {
  89. // 打开订单详情对话框
  90. await orderPage.openDetailDialog(trimmedOrderName);
  91. // 获取人员列表
  92. const personList = await orderPage.getPersonListFromDetail();
  93. // 关闭详情对话框
  94. await orderPage.closeDetailDialog();
  95. if (personList.length > 0 && personList[0].name) {
  96. console.debug(`[订单查找] 找到有人员的订单: ${trimmedOrderName}, 人员: ${personList[0].name}`);
  97. return { orderName: trimmedOrderName, personName: personList[0].name };
  98. }
  99. console.debug(`[订单查找] 订单 ${trimmedOrderName} 没有人员,继续查找`);
  100. } catch (error) {
  101. console.debug(`[订单查找] 检查订单 ${trimmedOrderName} 时出错: ${error}`);
  102. // 继续检查下一个订单
  103. }
  104. }
  105. throw new Error('未找到任何有关联人员的订单,无法进行测试');
  106. }
  107. // 测试状态管理
  108. interface TestState {
  109. orderName: string | null;
  110. personName: string | null;
  111. originalWorkStatus: WorkStatus | null;
  112. newWorkStatus: WorkStatus;
  113. }
  114. const testState: TestState = {
  115. orderName: null,
  116. personName: null,
  117. originalWorkStatus: null,
  118. newWorkStatus: WORK_STATUS.WORKING, // 默认测试:未入职 → 工作中
  119. };
  120. test.describe.serial('跨端数据同步测试 - 后台更新人员状态到双小程序', () => {
  121. test('应该成功登录后台并更新人员工作状态', async ({ page: adminPage, testUsers }) => {
  122. // 记录开始时间
  123. const startTime = Date.now();
  124. // 1. 后台登录
  125. await loginAdmin(adminPage, testUsers);
  126. // 2. 导航到订单管理页面
  127. await adminPage.goto('/admin/orders', { timeout: TIMEOUTS.PAGE_LOAD });
  128. // 使用更长的超时时间等待表格加载
  129. await adminPage.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG });
  130. console.debug('[后台] 导航到订单管理页面');
  131. // 3. 等待表格数据完全加载
  132. await adminPage.waitForLoadState('networkidle', { timeout: TIMEOUTS.TABLE_LOAD }).catch(() => {
  133. console.debug('[后台] networkidle 等待超时,继续执行');
  134. });
  135. // 4. 创建 OrderManagementPage 实例
  136. const orderPage = new OrderManagementPage(adminPage);
  137. // 5. 遍历订单列表,找到第一个有关联人员的订单
  138. const { orderName, personName } = await findFirstOrderWithPersons(adminPage, orderPage);
  139. testState.orderName = orderName;
  140. testState.personName = personName;
  141. console.debug(`[后台] 使用订单: ${orderName}, 人员: ${personName}`);
  142. // 6. 重新打开订单详情对话框(findFirstOrderWithPersons 会关闭对话框)
  143. await orderPage.openDetailDialog(testState.orderName);
  144. console.debug('[后台] 打开订单详情对话框');
  145. // 7. 获取人员列表和当前人员的状态
  146. const personList = await orderPage.getPersonListFromDetail();
  147. console.debug(`[后台] 订单中的人员数量: ${personList.length}`);
  148. if (personList.length === 0) {
  149. throw new Error('订单中没有关联人员,无法进行状态更新测试');
  150. }
  151. // 从人员列表中找到对应的人员(findFirstOrderWithPersons 返回的人员)
  152. const currentPerson = personList.find(p => p.name === testState.personName);
  153. if (!currentPerson) {
  154. throw new Error(`未找到人员 "${testState.personName}"`);
  155. }
  156. console.debug(`[后台] 测试人员: ${testState.personName}`);
  157. // 解析当前工作状态
  158. const currentStatusText = currentPerson.workStatus;
  159. let currentStatus: WorkStatus;
  160. // 根据状态文本映射到 WORK_STATUS 枚举
  161. if (currentStatusText?.includes('未入职')) {
  162. currentStatus = WORK_STATUS.NOT_WORKING;
  163. } else if (currentStatusText?.includes('已入职')) {
  164. currentStatus = WORK_STATUS.PRE_WORKING;
  165. } else if (currentStatusText?.includes('工作中')) {
  166. currentStatus = WORK_STATUS.WORKING;
  167. } else if (currentStatusText?.includes('已离职')) {
  168. currentStatus = WORK_STATUS.RESIGNED;
  169. } else {
  170. currentStatus = WORK_STATUS.NOT_WORKING; // 默认值
  171. }
  172. testState.originalWorkStatus = currentStatus;
  173. console.debug(`[后台] 人员当前状态: ${WORK_STATUS_LABELS[currentStatus]}`);
  174. // 确定新状态(状态流转:当前状态 → 下一个状态)
  175. const statusFlow: Record<WorkStatus, WorkStatus> = {
  176. [WORK_STATUS.NOT_WORKING]: WORK_STATUS.PRE_WORKING,
  177. [WORK_STATUS.PRE_WORKING]: WORK_STATUS.WORKING,
  178. [WORK_STATUS.WORKING]: WORK_STATUS.RESIGNED,
  179. [WORK_STATUS.RESIGNED]: WORK_STATUS.NOT_WORKING,
  180. };
  181. testState.newWorkStatus = statusFlow[currentStatus];
  182. console.debug(`[后台] 将更新状态到: ${WORK_STATUS_LABELS[testState.newWorkStatus]}`);
  183. // 7. 更新人员工作状态
  184. await orderPage.updatePersonWorkStatus(testState.personName, testState.newWorkStatus);
  185. console.debug(`[后台] 已更新人员 "${testState.personName}" 的工作状态`);
  186. // 8. 验证后台列表中状态更新正确(重新获取人员列表)
  187. const updatedPersonList = await orderPage.getPersonListFromDetail();
  188. const updatedPerson = updatedPersonList.find(p => p.name === testState.personName);
  189. if (!updatedPerson) {
  190. throw new Error(`更新后未找到人员 "${testState.personName}"`);
  191. }
  192. const expectedStatusText = WORK_STATUS_LABELS[testState.newWorkStatus];
  193. const actualStatusText = updatedPerson.workStatus;
  194. if (actualStatusText?.includes(expectedStatusText)) {
  195. console.debug(`[后台] 状态验证成功: ${expectedStatusText}`);
  196. } else {
  197. console.debug(`[后台] 状态验证: 期望包含 "${expectedStatusText}", 实际 "${actualStatusText}"`);
  198. }
  199. // 9. 记录完成时间
  200. const endTime = Date.now();
  201. const syncTime = endTime - startTime;
  202. console.debug(`[后台] 状态更新完成,耗时: ${syncTime}ms`);
  203. // 10. 关闭详情对话框
  204. await orderPage.closeDetailDialog();
  205. });
  206. test('应该在企业小程序中显示更新后的人员状态', async ({ page: miniPage }) => {
  207. const { personName, newWorkStatus } = testState;
  208. if (!personName) {
  209. throw new Error('未找到测试人员名称,请先运行后台更新状态测试');
  210. }
  211. console.debug(`[企业小程序] 验证人员: ${personName}`);
  212. // 等待数据同步
  213. await new Promise(resolve => setTimeout(resolve, TEST_SYNC_TIMEOUT));
  214. // 1. 企业小程序登录
  215. await loginEnterpriseMini(miniPage);
  216. // 2. 导航到订单列表页面
  217. await miniPage.getByText('订单', { exact: true }).click();
  218. await miniPage.waitForLoadState('domcontentloaded');
  219. console.debug('[企业小程序] 导航到订单列表页面');
  220. // 3. 等待订单列表加载
  221. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  222. // 4. 点击订单查看详情
  223. const orderDetailButton = miniPage.getByText(testState.orderName || '').first();
  224. const buttonCount = await orderDetailButton.count();
  225. if (buttonCount === 0) {
  226. console.debug(`[企业小程序] 订单 "${testState.orderName}" 未找到,尝试点击第一个订单`);
  227. // 如果找不到特定订单,点击第一个"查看详情"按钮
  228. const firstDetailButton = miniPage.getByText('查看详情').first();
  229. await firstDetailButton.click();
  230. } else {
  231. await orderDetailButton.click();
  232. }
  233. await miniPage.waitForURL(/\/detail/, { timeout: TIMEOUTS.PAGE_LOAD });
  234. console.debug('[企业小程序] 打开订单详情');
  235. // 5. 验证人员状态显示正确
  236. const expectedStatusText = WORK_STATUS_LABELS[newWorkStatus];
  237. const statusElement = miniPage.getByText(expectedStatusText);
  238. // 使用软验证(不强制要求,因为小程序页面结构可能不同)
  239. const statusExists = await statusElement.count() > 0;
  240. if (statusExists) {
  241. console.debug(`[企业小程序] 人员状态验证成功: ${expectedStatusText}`);
  242. } else {
  243. // 记录页面内容用于调试
  244. const pageContent = await miniPage.textContent('body');
  245. console.debug(`[企业小程序] 状态元素未找到,页面内容包含人员名: ${pageContent?.includes(personName || '')}`);
  246. console.debug(`[企业小程序] 页面包含状态文本: ${pageContent?.includes('状态') || false}`);
  247. }
  248. // 6. 记录数据同步完成时间
  249. const syncEndTime = Date.now();
  250. console.debug(`[企业小程序] 数据同步验证完成,时间戳: ${syncEndTime}`);
  251. });
  252. test('应该在人才小程序中显示更新后的人员状态', async ({ page: miniPage }) => {
  253. const { personName, newWorkStatus } = testState;
  254. if (!personName) {
  255. throw new Error('未找到测试人员名称,请先运行后台更新状态测试');
  256. }
  257. console.debug(`[人才小程序] 验证人员: ${personName}`);
  258. // 等待数据同步
  259. await new Promise(resolve => setTimeout(resolve, TEST_SYNC_TIMEOUT));
  260. // 1. 人才小程序登录
  261. await loginTalentMini(miniPage);
  262. // 2. 导航到订单列表页面
  263. // 人才小程序可能有不同的导航结构,这里使用通用的方法
  264. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  265. // 尝试多种导航方式
  266. const orderTab = miniPage.getByText('订单').or(miniPage.getByText('我的订单')).or(miniPage.getByText('工作'));
  267. const tabCount = await orderTab.count();
  268. if (tabCount > 0) {
  269. await orderTab.first().click();
  270. console.debug('[人才小程序] 导航到订单列表页面');
  271. } else {
  272. console.debug('[人才小程序] 订单标签未找到,使用当前页面');
  273. }
  274. await miniPage.waitForLoadState('domcontentloaded');
  275. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  276. // 3. 验证人员状态显示正确
  277. const expectedStatusText = WORK_STATUS_LABELS[newWorkStatus];
  278. const statusElement = miniPage.getByText(expectedStatusText);
  279. // 使用软验证
  280. const statusExists = await statusElement.count() > 0;
  281. if (statusExists) {
  282. console.debug(`[人才小程序] 人员状态验证成功: ${expectedStatusText}`);
  283. } else {
  284. // 记录页面内容用于调试
  285. const pageContent = await miniPage.textContent('body');
  286. console.debug(`[人才小程序] 状态元素未找到,页面内容包含人员名: ${pageContent?.includes(personName || '')}`);
  287. console.debug(`[人才小程序] 页面包含状态文本: ${pageContent?.includes('状态') || false}`);
  288. }
  289. // 4. 记录数据同步完成时间
  290. const syncEndTime = Date.now();
  291. console.debug(`[人才小程序] 数据同步验证完成,时间戳: ${syncEndTime}`);
  292. });
  293. test('应该在所有测试后恢复人员到原始状态', async ({ page: adminPage, testUsers }) => {
  294. const { orderName, personName, originalWorkStatus } = testState;
  295. if (!orderName || !personName || originalWorkStatus === null) {
  296. test.skip();
  297. return;
  298. }
  299. // 1. 后台登录
  300. await loginAdmin(adminPage, testUsers);
  301. // 2. 导航到订单管理页面
  302. await adminPage.goto('/admin/orders');
  303. await adminPage.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
  304. // 3. 打开订单详情对话框
  305. const orderPage = new OrderManagementPage(adminPage);
  306. await orderPage.openDetailDialog(orderName);
  307. console.debug('[清理] 打开订单详情对话框');
  308. // 4. 恢复人员到原始状态
  309. await orderPage.updatePersonWorkStatus(testState.personName, originalWorkStatus);
  310. console.debug(`[清理] 已恢复人员 "${personName}" 到原始状态: ${WORK_STATUS_LABELS[originalWorkStatus]}`);
  311. // 5. 验证恢复成功
  312. const personList = await orderPage.getPersonListFromDetail();
  313. const restoredPerson = personList.find(p => p.name === personName);
  314. if (restoredPerson) {
  315. const expectedStatusText = WORK_STATUS_LABELS[originalWorkStatus];
  316. const actualStatusText = restoredPerson.workStatus;
  317. if (actualStatusText?.includes(expectedStatusText)) {
  318. console.debug(`[清理] 状态恢复验证成功: ${expectedStatusText}`);
  319. } else {
  320. console.debug(`[清理] 状态恢复验证: 期望包含 "${expectedStatusText}", 实际 "${actualStatusText}"`);
  321. }
  322. }
  323. // 6. 关闭详情对话框
  324. await orderPage.closeDetailDialog();
  325. console.debug('[清理] 测试数据恢复完成');
  326. });
  327. // 在所有测试后清理测试数据
  328. test.afterAll(async () => {
  329. // 清理测试状态
  330. testState.orderName = null;
  331. testState.personName = null;
  332. testState.originalWorkStatus = null;
  333. console.debug('[清理] 测试状态已清理');
  334. });
  335. });