statistics-cross-system-validation.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { TIMEOUTS } from '../../utils/timeouts';
  2. import { test, expect } from '../../utils/test-setup';
  3. import type { Page } from '@playwright/test';
  4. import { AdminLoginPage } from '../../pages/admin/login.page';
  5. import { DisabilityPersonManagementPage } from '../../pages/admin/disability-person.page';
  6. import { EnterpriseMiniPage } from '../../pages/mini/enterprise-mini.page';
  7. /**
  8. * 跨系统数据一致性验证测试 (Story 13.12 任务 12-15)
  9. *
  10. * 测试目标:验证后台操作后,小程序数据统计页的数据是否正确更新
  11. *
  12. * 测试场景:
  13. * 1. 后台添加人员 → 小程序统计更新验证
  14. * 2. 修改人员状态 → 统计数据变化验证
  15. * 3. 数据删除后的统计更新
  16. *
  17. * 测试要点:
  18. * - 使用两个独立的 browser context(后台和小程序)
  19. * - 记录数据变化前后的统计值
  20. * - 验证数据同步的准确性
  21. */
  22. // 测试常量
  23. const TEST_SYNC_TIMEOUT = 10000; // 数据同步等待时间(ms)
  24. const TEST_POLL_INTERVAL = 500; // 轮询检查间隔(ms)
  25. // 小程序登录凭据
  26. const ENTERPRISE_LOGIN_PHONE = '13800001111';
  27. const ENTERPRISE_LOGIN_PASSWORD = process.env.TEST_ENTERPRISE_PASSWORD || 'password123';
  28. // 后台登录凭据
  29. const ADMIN_USERNAME = process.env.TEST_ADMIN_USERNAME || 'admin';
  30. const ADMIN_PASSWORD = process.env.TEST_ADMIN_PASSWORD || 'admin123';
  31. /**
  32. * 后台登录辅助函数
  33. */
  34. async function loginAdmin(page: Page) {
  35. const adminLoginPage = new AdminLoginPage(page);
  36. await adminLoginPage.goto();
  37. await adminLoginPage.login(ADMIN_USERNAME, ADMIN_PASSWORD);
  38. await adminLoginPage.expectLoginSuccess();
  39. console.debug('[后台] 登录成功');
  40. }
  41. /**
  42. * 小程序登录辅助函数
  43. */
  44. async function loginEnterpriseMini(miniPage: EnterpriseMiniPage) {
  45. await miniPage.goto();
  46. await miniPage.login(ENTERPRISE_LOGIN_PHONE, ENTERPRISE_LOGIN_PASSWORD);
  47. await miniPage.expectLoginSuccess();
  48. return miniPage;
  49. }
  50. // 测试状态管理
  51. interface TestState {
  52. initialEmploymentCount: number | null;
  53. updatedEmploymentCount: number | null;
  54. personId: number | null;
  55. personName: string | null;
  56. syncTime: number | null;
  57. }
  58. const testState: TestState = {
  59. initialEmploymentCount: null,
  60. updatedEmploymentCount: null,
  61. personId: null,
  62. personName: null,
  63. syncTime: null,
  64. };
  65. // 生成唯一的测试数据
  66. function generateTestPerson() {
  67. const timestamp = Date.now();
  68. return {
  69. name: `统计测试_${timestamp}`,
  70. gender: '男',
  71. idCard: `110101199001011${String(timestamp).slice(-2)}`,
  72. disabilityId: `1234567890${String(timestamp).slice(-4)}`,
  73. disabilityType: '肢体残疾',
  74. disabilityLevel: '一级',
  75. phone: `138${String(timestamp).slice(-8)}`,
  76. idAddress: `广东省广州市测试路${timestamp % 100}号`,
  77. province: '广东省',
  78. city: '广州市',
  79. };
  80. }
  81. test.describe('跨系统数据一致性验证 - Story 13.12 (任务 12-15)', () => {
  82. // 在所有测试后清理测试数据
  83. test.afterAll(async ({ adminPage }) => {
  84. // 清理测试数据
  85. if (testState.personId && testState.personName) {
  86. try {
  87. await loginAdmin(adminPage);
  88. const personPage = new DisabilityPersonManagementPage(adminPage);
  89. await personPage.goto();
  90. // 搜索并删除测试人员
  91. await personPage.searchByName(testState.personName);
  92. await personPage.page.waitForTimeout(TIMEOUTS.SHORT);
  93. // 检查是否找到该人员
  94. const exists = await personPage.personExists(testState.personName);
  95. if (exists) {
  96. await personPage.deleteDisabilityPerson(testState.personName);
  97. console.debug(`[清理] 已删除测试人员: ${testState.personName}`);
  98. }
  99. } catch (error) {
  100. console.debug(`[清理] 删除测试人员失败: ${error}`);
  101. }
  102. }
  103. // 重置测试状态
  104. testState.initialEmploymentCount = null;
  105. testState.updatedEmploymentCount = null;
  106. testState.personId = null;
  107. testState.personName = null;
  108. testState.syncTime = null;
  109. console.debug('[清理] 测试数据已清理');
  110. });
  111. test.describe.serial('任务 12: 后台添加人员 → 小程序统计更新验证', () => {
  112. test('步骤 1: 获取小程序初始统计数据', async ({ enterpriseMiniPage: page }) => {
  113. const miniPage = await loginEnterpriseMini(page);
  114. await miniPage.navigateToStatisticsPage();
  115. await miniPage.waitForStatisticsDataLoaded();
  116. // 记录初始在职人数
  117. const initialCount = await miniPage.getEmploymentCount();
  118. testState.initialEmploymentCount = initialCount;
  119. console.debug(`[小程序] 初始在职人数: ${initialCount ?? '无数据'}`);
  120. });
  121. test('步骤 2: 后台添加在职人员', async ({ adminPage }) => {
  122. await loginAdmin(adminPage);
  123. const personPage = new DisabilityPersonManagementPage(adminPage);
  124. await personPage.goto();
  125. // 生成测试人员数据
  126. const testPerson = generateTestPerson();
  127. testState.personName = testPerson.name;
  128. // 打开创建对话框
  129. await personPage.openCreateDialog();
  130. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  131. // 填写表单
  132. await personPage.fillBasicForm(testPerson);
  133. // 提交表单
  134. const result = await personPage.submitForm();
  135. // 验证创建成功
  136. expect(result.hasSuccess || !result.hasError).toBe(true);
  137. console.debug(`[后台] 创建人员成功: ${testPerson.name}`);
  138. // 等待人员记录出现
  139. const personExists = await personPage.waitForPersonExists(testPerson.name, {
  140. timeout: TIMEOUTS.TABLE_LOAD,
  141. });
  142. expect(personExists).toBe(true);
  143. // 搜索人员获取 ID
  144. await personPage.searchByName(testPerson.name);
  145. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  146. console.debug(`[后台] 人员已添加: ${testPerson.name}`);
  147. });
  148. test('步骤 3: 验证小程序统计数据更新', async ({ enterpriseMiniPage: page }) => {
  149. const miniPage = await loginEnterpriseMini(page);
  150. await miniPage.navigateToStatisticsPage();
  151. await miniPage.waitForStatisticsDataLoaded();
  152. // 记录同步开始时间
  153. const syncStartTime = Date.now();
  154. // 轮询检查统计数据是否更新
  155. let updatedCount: number | null = null;
  156. let found = false;
  157. for (let elapsed = 0; elapsed <= TEST_SYNC_TIMEOUT; elapsed += TEST_POLL_INTERVAL) {
  158. // 强制刷新统计数据
  159. await miniPage.forceRefreshStatistics();
  160. await miniPage.waitForStatisticsDataLoaded();
  161. updatedCount = await miniPage.getEmploymentCount();
  162. // 检查数据是否更新(在职人数应该增加)
  163. if (testState.initialEmploymentCount !== null &&
  164. updatedCount !== null &&
  165. updatedCount > testState.initialEmploymentCount) {
  166. found = true;
  167. testState.syncTime = Date.now() - syncStartTime;
  168. console.debug(`[小程序] 统计数据已更新,耗时: ${testState.syncTime}ms`);
  169. break;
  170. }
  171. await page.page.waitForTimeout(TEST_POLL_INTERVAL);
  172. }
  173. testState.updatedEmploymentCount = updatedCount;
  174. // 验证数据已更新 - 精确验证人数增加 1
  175. expect(found, `在职人数应该从 ${testState.initialEmploymentCount} 增加`).toBe(true);
  176. if (testState.initialEmploymentCount !== null && updatedCount !== null) {
  177. expect(updatedCount).toBe(testState.initialEmploymentCount + 1);
  178. }
  179. console.debug(`[验证] 初始在职人数: ${testState.initialEmploymentCount}`);
  180. console.debug(`[验证] 更新后在职人数: ${updatedCount}`);
  181. console.debug(`[验证] 在职人数增加: ${(updatedCount ?? 0) - (testState.initialEmploymentCount ?? 0)}`);
  182. });
  183. });
  184. test.describe.serial('任务 13: 修改人员状态 → 统计数据变化验证', () => {
  185. test('步骤 1: 获取小程序当前统计数据', async ({ enterpriseMiniPage: miniPage }) => {
  186. await loginEnterpriseMini(miniPage);
  187. await miniPage.navigateToStatisticsPage();
  188. await miniPage.waitForStatisticsDataLoaded();
  189. const currentCount = await miniPage.getEmploymentCount();
  190. testState.initialEmploymentCount = currentCount;
  191. console.debug(`[小程序] 当前在职人数: ${currentCount ?? '无数据'}`);
  192. });
  193. test('步骤 2: 后台修改人员状态为未在职', async ({ adminPage }) => {
  194. if (!testState.personName) {
  195. console.debug('[跳过] 未找到测试人员,跳过状态修改测试');
  196. test.skip();
  197. return;
  198. }
  199. await loginAdmin(adminPage);
  200. const personPage = new DisabilityPersonManagementPage(adminPage);
  201. await personPage.goto();
  202. // 搜索测试人员
  203. await personPage.searchByName(testState.personName);
  204. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  205. // 打开编辑对话框
  206. await personPage.openEditDialog(testState.personName);
  207. await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
  208. // 修改人员工作状态为"未在职"(离职)
  209. const result = await personPage.updateWorkStatus(false);
  210. // 验证修改成功
  211. expect(result.hasSuccess || !result.hasError).toBe(true);
  212. console.debug(`[后台] 已修改人员状态为未在职: ${testState.personName}`);
  213. });
  214. test('步骤 3: 验证小程序在职人数减少', async ({ enterpriseMiniPage: miniPage }) => {
  215. if (!testState.personName) {
  216. test.skip();
  217. return;
  218. }
  219. await loginEnterpriseMini(miniPage);
  220. await miniPage.navigateToStatisticsPage();
  221. await miniPage.waitForStatisticsDataLoaded();
  222. // 记录同步开始时间
  223. const syncStartTime = Date.now();
  224. // 轮询检查统计数据是否更新
  225. let updatedCount: number | null = null;
  226. let found = false;
  227. for (let elapsed = 0; elapsed <= TEST_SYNC_TIMEOUT; elapsed += TEST_POLL_INTERVAL) {
  228. // 强制刷新统计数据
  229. await miniPage.forceRefreshStatistics();
  230. await miniPage.waitForStatisticsDataLoaded();
  231. updatedCount = await miniPage.getEmploymentCount();
  232. // 检查数据是否更新(在职人数应该减少)
  233. if (testState.initialEmploymentCount !== null &&
  234. updatedCount !== null &&
  235. updatedCount < testState.initialEmploymentCount) {
  236. found = true;
  237. testState.syncTime = Date.now() - syncStartTime;
  238. console.debug(`[小程序] 统计数据已更新,耗时: ${testState.syncTime}ms`);
  239. break;
  240. }
  241. await miniPage.page.waitForTimeout(TEST_POLL_INTERVAL);
  242. }
  243. testState.updatedEmploymentCount = updatedCount;
  244. // 验证数据已更新
  245. expect(found, `在职人数应该从 ${testState.initialEmploymentCount} 减少`).toBe(true);
  246. if (testState.initialEmploymentCount !== null) {
  247. expect(updatedCount).toBeLessThan(testState.initialEmploymentCount);
  248. }
  249. console.debug(`[验证] 修改前在职人数: ${testState.initialEmploymentCount}`);
  250. console.debug(`[验证] 修改后在职人数: ${updatedCount}`);
  251. console.debug(`[验证] 在职人数减少: ${(testState.initialEmploymentCount ?? 0) - (updatedCount ?? 0)}`);
  252. });
  253. });
  254. test.describe.serial('任务 14: 边界条件测试', () => {
  255. test('无数据场景:切换到无数据的年份', async ({ enterpriseMiniPage: miniPage }) => {
  256. await loginEnterpriseMini(miniPage);
  257. await miniPage.navigateToStatisticsPage();
  258. await miniPage.waitForStatisticsDataLoaded();
  259. // 切换到较早的年份(可能无数据)
  260. await miniPage.selectYear(2020);
  261. await miniPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
  262. // 验证页面不会崩溃
  263. const cards = await miniPage.getStatisticsCards();
  264. expect(cards.length).toBeGreaterThan(0);
  265. // 验证显示无数据状态
  266. const employmentCount = await miniPage.getEmploymentCount();
  267. expect(employmentCount === null || employmentCount === 0).toBeTruthy();
  268. console.debug('[验证] 无数据场景测试通过');
  269. });
  270. test('跨年跨月:切换年份和月份', async ({ enterpriseMiniPage: miniPage }) => {
  271. await loginEnterpriseMini(miniPage);
  272. await miniPage.navigateToStatisticsPage();
  273. await miniPage.waitForStatisticsDataLoaded();
  274. // 切换到下一年
  275. const nextYear = new Date().getFullYear() + 1;
  276. await miniPage.selectYear(nextYear);
  277. await miniPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
  278. // 切换到1月
  279. await miniPage.selectMonth(1);
  280. await miniPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
  281. // 验证页面正常显示
  282. const cards = await miniPage.getStatisticsCards();
  283. expect(cards.length).toBeGreaterThan(0);
  284. console.debug('[验证] 跨年跨月测试通过');
  285. });
  286. });
  287. test.describe.serial('任务 15: 数据一致性验证方法测试', () => {
  288. test('验证 validateStatisticsAccuracy 方法', async ({ enterpriseMiniPage: miniPage }) => {
  289. await loginEnterpriseMini(miniPage);
  290. await miniPage.navigateToStatisticsPage();
  291. await miniPage.waitForStatisticsDataLoaded();
  292. // 获取当前实际数据
  293. const actualEmploymentCount = await miniPage.getEmploymentCount();
  294. const actualAverageSalary = await miniPage.getAverageSalary();
  295. console.debug('[任务 15.1] 实际数据:', {
  296. employmentCount: actualEmploymentCount,
  297. averageSalary: actualAverageSalary,
  298. });
  299. // 测试验证方法 - 应该与实际数据匹配
  300. if (actualEmploymentCount !== null) {
  301. const result1 = await miniPage.validateStatisticsAccuracy({
  302. employmentCount: actualEmploymentCount,
  303. });
  304. expect(result1.passed).toBe(true);
  305. expect(result1.details.employmentCount?.match).toBe(true);
  306. }
  307. if (actualAverageSalary !== null) {
  308. const result2 = await miniPage.validateStatisticsAccuracy({
  309. averageSalary: actualAverageSalary,
  310. });
  311. expect(result2.passed).toBe(true);
  312. expect(result2.details.averageSalary?.match).toBe(true);
  313. }
  314. // 测试验证方法 - 应该与错误数据不匹配
  315. const result3 = await miniPage.validateStatisticsAccuracy({
  316. employmentCount: 999999, // 明显错误的值
  317. });
  318. expect(result3.passed).toBe(false);
  319. expect(result3.details.employmentCount?.match).toBe(false);
  320. console.debug('[任务 15] ✓ 数据一致性验证方法测试通过');
  321. });
  322. });
  323. test.describe.serial('综合测试:完整的跨系统数据验证流程', () => {
  324. test('后台操作 → 小程序统计验证的完整流程', async ({ adminPage, enterpriseMiniPage: miniPage }) => {
  325. // 此测试验证完整的跨系统数据同步流程
  326. // 1. 获取初始统计
  327. await loginEnterpriseMini(miniPage);
  328. await miniPage.navigateToStatisticsPage();
  329. await miniPage.waitForStatisticsDataLoaded();
  330. const initialStats = {
  331. employmentCount: await miniPage.getEmploymentCount(),
  332. averageSalary: await miniPage.getAverageSalary(),
  333. employmentRate: await miniPage.getEmploymentRate(),
  334. };
  335. console.debug('[综合测试] 步骤 1: 初始统计数据:', initialStats);
  336. // 2. 后台操作(这里只是示例,实际需要添加或修改数据)
  337. await loginAdmin(adminPage);
  338. const personPage = new DisabilityPersonManagementPage(adminPage);
  339. await personPage.goto();
  340. console.debug('[综合测试] 步骤 2: 后台操作完成');
  341. // 3. 验证小程序统计更新
  342. await miniPage.forceRefreshStatistics();
  343. await miniPage.waitForStatisticsDataLoaded();
  344. const updatedStats = {
  345. employmentCount: await miniPage.getEmploymentCount(),
  346. averageSalary: await miniPage.getAverageSalary(),
  347. employmentRate: await miniPage.getEmploymentRate(),
  348. };
  349. console.debug('[综合测试] 步骤 3: 更新后统计数据:', updatedStats);
  350. // 4. 验证数据一致性
  351. const validation = await miniPage.validateStatisticsAccuracy({
  352. employmentCount: updatedStats.employmentCount ?? undefined,
  353. averageSalary: updatedStats.averageSalary ?? undefined,
  354. });
  355. expect(validation.passed).toBe(true);
  356. console.debug('[综合测试] ✓ 完整的跨系统数据验证流程通过');
  357. });
  358. });
  359. });