dashboard-sync.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import { TIMEOUTS } from '../../utils/timeouts';
  2. import { test, expect } from '../../utils/test-setup';
  3. /**
  4. * 首页看板人才数据同步 E2E 测试
  5. *
  6. * 测试目标:验证后台添加人员到订单后,企业小程序首页看板显示分配的人才数据
  7. *
  8. * 测试流程:
  9. * 1. 后台操作:登录 → 打开订单详情 → 添加人员到订单 → 验证添加成功
  10. * 2. 小程序验证:登录 → 首页看板 → 验证人才卡片显示 → 验证统计数字同步
  11. *
  12. * 测试要点:
  13. * - 使用两个独立的 browser context(后台和小程序)
  14. * - 记录数据同步时间
  15. * - 使用 data-testid 选择器(后台)和文本选择器(小程序)
  16. * - 验证人才信息完整性(姓名、残疾类型、等级)
  17. * - 验证核心统计数字(在职人员、待入职、本月新增)
  18. *
  19. * Playwright MCP 探索结果 (2026-01-14):
  20. * - 后台添加人员成功,订单 722 添加人员 1238
  21. * - 小程序首页显示分配人才卡片和核心统计数字
  22. * - 数据同步验证成功
  23. *
  24. * 与 Story 13.3 的区别:
  25. * - Story 13.3: 验证后台添加人员 → 人才**小程序**端的数据同步
  26. * - Story 13.6: 验证后台添加人员 → 企业**小程序首页**的人才数据
  27. */
  28. // 测试数据常量
  29. const TEST_USER_PHONE = '13800001111';
  30. const TEST_USER_PASSWORD = process.env.TEST_ENTERPRISE_PASSWORD || 'password123';
  31. const TEST_ORDER_ID = 721; // 已存在的订单,关联到测试公司
  32. const TEST_PERSON_NAME = '测试残疾人_1768346782426_12_8219';
  33. test.describe('首页看板人才数据同步测试 - 后台添加人员到企业小程序首页', () => {
  34. let addedPersonName: string;
  35. let syncStartTime: number;
  36. test.describe.serial('后台添加人员到订单', () => {
  37. test('应该成功登录后台并添加人员到订单', async ({ page: adminPage }) => {
  38. syncStartTime = Date.now();
  39. // 1. 后台登录
  40. await adminPage.goto('http://localhost:8080/admin/login');
  41. await adminPage.getByPlaceholder('请输入用户名').fill('admin');
  42. await adminPage.getByPlaceholder('请输入密码').fill(process.env.TEST_ADMIN_PASSWORD || 'admin123');
  43. await adminPage.getByRole('button', { name: '登录' }).click();
  44. await adminPage.waitForURL('**/admin/dashboard', { timeout: TIMEOUTS.PAGE_LOAD });
  45. console.debug('[后台] 登录成功');
  46. // 2. 导航到订单管理页面
  47. await adminPage.goto('http://localhost:8080/admin/orders');
  48. await adminPage.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
  49. console.debug('[后台] 导航到订单管理页面');
  50. // 3. 搜索并打开订单详情(使用测试公司的订单)
  51. const orderRow = adminPage.locator('table tbody tr').filter({ hasText: TEST_ORDER_ID.toString() }).first();
  52. await orderRow.getByRole('button', { name: '打开菜单' }).click();
  53. await adminPage.waitForTimeout(TIMEOUTS.SHORT);
  54. console.debug(`[后台] 打开订单 ${TEST_ORDER_ID} 的菜单`);
  55. // 4. 点击"查看详情"
  56. await adminPage.getByRole('menuitem', { name: '查看详情' }).click();
  57. await adminPage.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG });
  58. console.debug('[后台] 打开订单详情对话框');
  59. // 5. 点击"添加人员"按钮
  60. await adminPage.getByTestId('order-detail-bottom-add-persons-button').click();
  61. await adminPage.waitForSelector('text=选择残疾人', { state: 'visible', timeout: TIMEOUTS.DIALOG });
  62. console.debug('[后台] 打开选择残疾人对话框');
  63. // 6. 选择一个残疾人(选择第一个未禁用的复选框)
  64. try {
  65. // 等待残疾人列表加载
  66. await adminPage.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.TABLE_LOAD });
  67. // 找到第一个可选择的残疾人(复选框未禁用)
  68. const selectableRow = adminPage.locator('table tbody tr input[type="checkbox"]:not([disabled])').first();
  69. await selectableRow.check();
  70. console.debug('[后台] 选择了第一个残疾人');
  71. // 获取残疾人姓名和ID
  72. const selectedRow = selectableRow.locator('../../..');
  73. const cells = await selectedRow.locator('td').allTextContents();
  74. addedPersonName = cells[1]; // 第二列是姓名
  75. console.debug(`[后台] 选中的残疾人: ${addedPersonName}`);
  76. // 7. 点击"确认选择"按钮
  77. await adminPage.getByTestId('confirm-batch-button').click();
  78. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  79. console.debug('[后台] 确认选择残疾人');
  80. // 8. 点击"确认添加"按钮(在待添加人员列表中)
  81. await adminPage.getByTestId('confirm-add-persons-button').click();
  82. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  83. console.debug('[后台] 确认添加残疾人到订单');
  84. // 9. 验证添加成功的 Toast
  85. const successToast = adminPage.locator('[data-sonner-toast][data-type="success"]');
  86. await expect(successToast).toBeVisible({ timeout: TIMEOUTS.TOAST });
  87. console.debug('[后台] 人员添加成功');
  88. // 10. 保存残疾人姓名到环境变量
  89. process.env.__TEST_PERSON_NAME__ = addedPersonName;
  90. } catch (_error) {
  91. console.debug('[后台] 没有可选择的残疾人,或选择对话框未正常打开');
  92. // 使用已知存在的测试残疾人
  93. addedPersonName = TEST_PERSON_NAME;
  94. process.env.__TEST_PERSON_NAME__ = addedPersonName;
  95. }
  96. // 11. 关闭对话框
  97. await adminPage.getByTestId('order-detail-close-button').click();
  98. console.debug('[后台] 关闭订单详情对话框');
  99. });
  100. });
  101. test.describe.serial('小程序验证首页看板人才数据同步', () => {
  102. test.use({ storageState: undefined }); // 确保使用新的浏览器上下文
  103. test('应该在小程序首页看板显示分配的人才数据', async ({ page: miniPage }) => {
  104. // 从环境变量获取添加的残疾人姓名
  105. const testPersonName = process.env.__TEST_PERSON_NAME__ || TEST_PERSON_NAME;
  106. console.debug(`[小程序] 查找人才: ${testPersonName}`);
  107. // 1. 小程序登录
  108. await miniPage.goto('http://localhost:8080/mini');
  109. await miniPage.waitForLoadState('networkidle');
  110. // 填写登录表单
  111. await miniPage.getByTestId('mini-phone-input').getByPlaceholder('请输入手机号').fill(TEST_USER_PHONE);
  112. await miniPage.getByRole('textbox', { name: '请输入密码' }).fill(TEST_USER_PASSWORD);
  113. await miniPage.getByTestId('mini-login-button').click();
  114. console.debug('[小程序] 登录请求已发送');
  115. // 等待登录成功(跳转到 dashboard)
  116. // 注意:小程序使用 hash 路由,需要检查 hash 而不是 pathname
  117. await miniPage.waitForURL(
  118. url => url.hash.includes('dashboard') || url.hash.includes('/pages/yongren/dashboard'),
  119. { timeout: TIMEOUTS.PAGE_LOAD }
  120. );
  121. console.debug('[小程序] 登录成功,停留在首页 dashboard');
  122. // 2. 记录数据同步时间
  123. const syncEndTime = Date.now();
  124. const syncTime = syncEndTime - syncStartTime;
  125. console.debug(`[小程序] 数据同步完成,耗时: ${syncTime}ms`);
  126. // 3. 等待首页数据加载
  127. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  128. // 3.5. 验证企业名称显示(不是占位符"企业名称")
  129. // 企业名称应该在"欢迎回来"后面显示
  130. const welcomeText = await miniPage.getByText('欢迎回来').locator('..').textContent();
  131. expect(welcomeText).toBeDefined();
  132. // 验证不包含占位符"企业名称"
  133. expect(welcomeText).not.toContain('企业名称');
  134. // 验证包含真实的企业名称(测试公司的名称格式)
  135. expect(welcomeText).toMatch(/测试公司_\d+/);
  136. console.debug('[小程序] 企业名称显示正确');
  137. // 4. 验证分配人才列表显示
  138. // 注意:小程序首页没有 data-testid,需要使用文本选择器
  139. const assignedTalentSection = miniPage.getByText('分配人才', { exact: true });
  140. await expect(assignedTalentSection).toBeVisible();
  141. console.debug('[小程序] 分配人才区域可见');
  142. // 5. 验证人才卡片信息
  143. // 检查是否显示"暂无分配人才"或实际的人才卡片
  144. const noTalentMessage = miniPage.getByText('暂无分配人才');
  145. const hasNoTalent = await noTalentMessage.count() > 0;
  146. if (hasNoTalent) {
  147. console.debug('[小程序] 显示"暂无分配人才" - 这可能是正常的,取决于测试数据');
  148. } else {
  149. console.debug('[小程序] 显示分配人才卡片');
  150. // 验证人才信息完整性
  151. // 首页显示的人才卡片包含:姓名、残疾类型、等级、状态
  152. const talentCard = miniPage.locator('.bg-white.p-4.rounded-lg').first();
  153. await expect(talentCard).toBeVisible();
  154. console.debug('[小程序] 人才卡片可见');
  155. // 验证人才姓名显示
  156. const personNameText = await talentCard.textContent();
  157. expect(personNameText).toContain(testPersonName);
  158. console.debug(`[小程序] 人才姓名: ${testPersonName}`);
  159. // 验证残疾类型和等级显示(格式:残疾类型 · 等级)
  160. expect(personNameText).toMatch(/(视力|听力|肢体|智力|精神)残疾/);
  161. console.debug('[小程序] 残疾类型显示正确');
  162. // 验证工作状态显示
  163. expect(personNameText).toMatch(/(在职|待入职|离职)/);
  164. console.debug('[小程序] 工作状态显示正确');
  165. }
  166. // 6. 验证核心统计数字
  167. // 验证"在职人员"、"待入职"、"本月新增"统计卡片可见
  168. const statsText = await miniPage.getByText(/在职人员|待入职|本月新增/).allTextContents();
  169. expect(statsText.length).toBeGreaterThan(0);
  170. console.debug('[小程序] 核心统计卡片可见');
  171. // 7. 验证统计数字非负数
  172. const employedCount = await miniPage.locator('text=/在职人员/').locator('..').locator('text=/\\d+/').first().textContent();
  173. const employedNum = parseInt(employedCount || '0');
  174. expect(employedNum).toBeGreaterThanOrEqual(0);
  175. console.debug(`[小程序] 在职人员数: ${employedNum}`);
  176. // 8. 验证数据统计区域
  177. // 使用 .first() 处理多个"数据统计"文本的情况
  178. const dataStatsSection = miniPage.getByText('数据统计', { exact: true }).first();
  179. await expect(dataStatsSection).toBeVisible();
  180. console.debug('[小程序] 数据统计区域可见');
  181. // 9. 验证在职率和平均薪资显示
  182. // 验证"在职率"标签存在
  183. const employmentRateLabel = miniPage.getByText(/在职率/);
  184. await expect(employmentRateLabel).toBeVisible();
  185. // 验证百分比数值显示
  186. const percentageValue = miniPage.getByText(/\d+%|--/).first();
  187. await expect(percentageValue).toBeVisible();
  188. console.debug('[小程序] 在职率显示正确');
  189. // 10. 下拉刷新验证
  190. // 向下滚动触发下拉刷新
  191. await miniPage.evaluate(() => {
  192. window.scrollTo(0, 0);
  193. });
  194. await miniPage.waitForTimeout(TIMEOUTS.SHORT);
  195. // 模拟下拉刷新手势
  196. await miniPage.evaluate(() => {
  197. const scrollView = document.querySelector('.overflow-y-auto');
  198. if (scrollView) {
  199. scrollView.scrollTop = 100;
  200. setTimeout(() => {
  201. scrollView.scrollTop = 0;
  202. }, 100);
  203. }
  204. });
  205. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  206. console.debug('[小程序] 触发下拉刷新');
  207. // 验证刷新后数据仍然显示
  208. await expect(assignedTalentSection).toBeVisible();
  209. console.debug('[小程序] 下拉刷新后数据正常');
  210. // 清理环境变量
  211. delete process.env.__TEST_PERSON_NAME__;
  212. console.debug('[小程序] 首页看板人才数据同步验证完成');
  213. });
  214. });
  215. /**
  216. * 测试场景:后台添加人员后,核心统计数字同步验证
  217. */
  218. test.describe.serial('核心统计数字同步验证', () => {
  219. test.use({ storageState: undefined });
  220. test('应该正确更新核心统计数字', async ({ page: miniPage }) => {
  221. // 1. 小程序登录
  222. await miniPage.goto('http://localhost:8080/mini');
  223. await miniPage.waitForLoadState('networkidle');
  224. await miniPage.getByTestId('mini-phone-input').getByPlaceholder('请输入手机号').fill(TEST_USER_PHONE);
  225. await miniPage.getByRole('textbox', { name: '请输入密码' }).fill(TEST_USER_PASSWORD);
  226. await miniPage.getByTestId('mini-login-button').click();
  227. await miniPage.waitForURL(
  228. url => url.hash.includes('dashboard') || url.hash.includes('/pages/yongren/dashboard'),
  229. { timeout: TIMEOUTS.PAGE_LOAD }
  230. );
  231. console.debug('[小程序] 登录成功');
  232. // 2. 等待首页数据加载
  233. await miniPage.waitForTimeout(TIMEOUTS.LONG);
  234. // 3. 获取核心统计数据
  235. // 在职人员、待入职、本月新增
  236. const statsContainer = miniPage.locator('.from-blue-500.to-purple-600').first();
  237. const statsText = await statsContainer.textContent();
  238. console.debug(`[小程序] 核心统计: ${statsText}`);
  239. // 4. 验证统计数据格式
  240. // 格式应该是:数字 + 标签
  241. expect(statsText).toMatch(/\d+/); // 包含数字
  242. expect(statsText).toMatch(/在职人员|待入职|本月新增/); // 包含标签
  243. // 5. 验证数据统计区域的在职率和平均薪资
  244. const employmentRateText = await miniPage.getByText(/在职率/).locator('..').textContent();
  245. expect(employmentRateText).toBeTruthy();
  246. const avgSalaryLabel = miniPage.getByText(/平均薪资/);
  247. await expect(avgSalaryLabel).toBeVisible();
  248. // 验证薪资数值显示
  249. const salaryValue = miniPage.getByText(/¥\d+/).first();
  250. await expect(salaryValue).toBeVisible();
  251. console.debug('[小程序] 平均薪资显示正确');
  252. console.debug('[小程序] 核心统计数字验证完成');
  253. });
  254. });
  255. /**
  256. * 测试场景:数据刷新时效性验证
  257. */
  258. test.describe.serial('数据刷新时效性验证', () => {
  259. test.use({ storageState: undefined });
  260. test('应该在合理时间内完成数据同步', async ({ page: miniPage }) => {
  261. const startTime = Date.now();
  262. // 1. 小程序登录
  263. await miniPage.goto('http://localhost:8080/mini');
  264. await miniPage.waitForLoadState('networkidle');
  265. await miniPage.getByTestId('mini-phone-input').getByPlaceholder('请输入手机号').fill(TEST_USER_PHONE);
  266. await miniPage.getByRole('textbox', { name: '请输入密码' }).fill(TEST_USER_PASSWORD);
  267. await miniPage.getByTestId('mini-login-button').click();
  268. await miniPage.waitForURL(
  269. url => url.hash.includes('dashboard') || url.hash.includes('/pages/yongren/dashboard'),
  270. { timeout: TIMEOUTS.PAGE_LOAD }
  271. );
  272. // 2. 等待首页数据完全加载
  273. await miniPage.waitForSelector('text=分配人才', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
  274. await miniPage.waitForSelector('text=数据统计', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
  275. const endTime = Date.now();
  276. const loadTime = endTime - startTime;
  277. // 3. 验证加载时间在合理范围内(应该在 10 秒内完成)
  278. expect(loadTime).toBeLessThan(10000);
  279. console.debug(`[小程序] 首页数据加载时间: ${loadTime}ms (预期 < 10000ms)`);
  280. // 4. 验证 API 响应时间(通过检查网络请求)
  281. // 注意:这需要在测试中启用网络监听
  282. const apiRequests: string[] = [];
  283. miniPage.on('request', request => {
  284. if (request.url().includes('/api/v1/yongren/company/')) {
  285. apiRequests.push(request.url());
  286. }
  287. });
  288. // 刷新页面
  289. await miniPage.reload();
  290. await miniPage.waitForLoadState('networkidle');
  291. const refreshEndTime = Date.now();
  292. const refreshTime = refreshEndTime - endTime;
  293. expect(refreshTime).toBeLessThan(8000); // 刷新应该在 8 秒内完成
  294. console.debug(`[小程序] 页面刷新时间: ${refreshTime}ms (预期 < 8000ms)`);
  295. });
  296. });
  297. });