energy-api.spec.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /**
  2. * 【API级】能量系统收支全流程
  3. * ================================================================
  4. * 对应 DOM 版:e2e/energy-system.spec.js(远程无 SPA,DOM 版不可跑)
  5. * 接口契约来源:EnergyController(/api/energy/*)
  6. * sandbox : {} 家庭能量沙盘(五维)
  7. * overview : { memberId? } 能量概览
  8. * logs : { memberId?, dimensionCode?, page?, size? } 能量流水(Page)
  9. * grant : { memberId, taskId, amount, dimensionCode } 任务完成发放能量
  10. * deduct-for-wish: { memberId, wishId, amount } 心愿兑换扣能量
  11. * 五维维度代码:body(身) / mind(心) / wisdom(智) / action(行) / wealth(富)
  12. * 运行:npx playwright test -c playwright.config.api.js --project=desktop
  13. */
  14. const { test, expect } = require('@playwright/test');
  15. const { registerRole, apiPost, pickMemberByUserId, uniqueSuffix } = require('../helpers/api');
  16. const DIMENSION = 'body'; // 身
  17. test.describe('【API级】能量系统收支全流程', () => {
  18. let ctx;
  19. let selfMemberId;
  20. test.beforeAll(async ({ request }) => {
  21. ctx = await registerRole(request, 'parent', 'QA-ENERGY');
  22. const list = await apiPost(request, '/api/family/member/list', {}, ctx.token);
  23. expect(list.code).toBe(200);
  24. const self = pickMemberByUserId(list.data, ctx.userId);
  25. expect(self, '成员列表应包含登录用户自身').toBeTruthy();
  26. selfMemberId = self.id;
  27. });
  28. // ===== 场景1:获取家庭能量沙盘 =====
  29. test('[场景1] 获取家庭能量沙盘 - 期望返回五维能量数据', async ({ request }) => {
  30. const r = await apiPost(request, '/api/energy/sandbox', {}, ctx.token);
  31. expect(r.code).toBe(200);
  32. expect(r.data).toBeTruthy();
  33. expect(typeof r.data).toBe('object');
  34. });
  35. test('[场景1b] 未加入家庭/越权访问 - 期望返回错误提示', async ({ request }) => {
  36. const r = await apiPost(request, '/api/family/member/list', { familyId: 999999999 }, ctx.token);
  37. expect(r.code).toBe(403);
  38. expect(String(r.message)).toContain('无权访问');
  39. });
  40. // ===== 场景2:任务完成能量发放 =====
  41. /**
  42. * 【已知缺陷 CFC-EN-001】/api/energy/grant 在远程稳定返回 code=500「服务器内部错误」,
  43. * 属后端未捕获异常(EnergyController.grantEnergy → EnergyService.grantEnergy
  44. * → awardEnergy → awardByConfig → executeAward)。
  45. * 用例保留「期望成功」的正确断言,修复后自动转绿;当前以 fixme 标记不阻塞 CI。
  46. */
  47. test('[场景2] 任务完成能量发放 - 期望能量增加且流水记录', async ({ request }) => {
  48. test.fixme(true, 'CFC-EN-001:/api/energy/grant 返回 500 服务器内部错误(后端未捕获异常),修复后移除本标记');
  49. const amount = 10;
  50. const taskId = Number(uniqueSuffix());
  51. const grant = await apiPost(
  52. request,
  53. '/api/energy/grant',
  54. { memberId: selfMemberId, taskId, amount, dimensionCode: DIMENSION },
  55. ctx.token
  56. );
  57. expect(grant.code, '能量发放应成功').toBe(200);
  58. expect(grant.data).toBeTruthy();
  59. // 流水应记录本次发放
  60. const logs = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, ctx.token);
  61. expect(logs.code).toBe(200);
  62. const records = (logs.data && logs.data.records) || [];
  63. expect(Array.isArray(records)).toBeTruthy();
  64. expect(records.some((r) => String(r.sourceId) === String(taskId))).toBeTruthy();
  65. });
  66. test('[场景2c] 已知缺陷复现 - 期望当前返回 500 服务器内部错误', async ({ request }) => {
  67. // 特征化用例:锁定当前(缺陷)行为;研发修复后本用例会失败,提示移除 场景2 的 fixme
  68. const r = await apiPost(
  69. request,
  70. '/api/energy/grant',
  71. { memberId: selfMemberId, taskId: Number(uniqueSuffix()), amount: 1, dimensionCode: DIMENSION },
  72. ctx.token
  73. );
  74. expect(r.code, 'CFC-EN-001 当前表现为 500').toBe(500);
  75. expect(String(r.message)).toContain('服务器内部错误');
  76. });
  77. test('[场景2b] 能量发放缺参 - 期望返回错误提示', async ({ request }) => {
  78. const r = await apiPost(request, '/api/energy/grant', { memberId: selfMemberId }, ctx.token);
  79. expect(r.code).not.toBe(200);
  80. expect(String(r.message)).toContain('不能为空');
  81. });
  82. // ===== 场景3:能量兑换心愿 =====
  83. test('[场景3] 能量兑换心愿 - 期望参数校验生效且未扣减能量', async ({ request }) => {
  84. // 3a) 缺参必须被拒
  85. const missing = await apiPost(
  86. request,
  87. '/api/energy/deduct-for-wish',
  88. { memberId: selfMemberId },
  89. ctx.token
  90. );
  91. expect(missing.code).not.toBe(200);
  92. expect(String(missing.message)).toContain('不能为空');
  93. // 3b) 心愿不存在时,接口以 code=200 + data=false 表达「未扣减」(布尔返回语义)
  94. const absent = await apiPost(
  95. request,
  96. '/api/energy/deduct-for-wish',
  97. { memberId: selfMemberId, wishId: 999999999, amount: 1 },
  98. ctx.token
  99. );
  100. expect(absent.code).toBe(200);
  101. expect(absent.data).toBe(false);
  102. });
  103. // ===== 场景4:查询能量流水 =====
  104. test('[场景4] 查询能量流水 - 期望返回分页记录', async ({ request }) => {
  105. const r = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId, page: 1, size: 10 }, ctx.token);
  106. expect(r.code).toBe(200);
  107. expect(r.data).toBeTruthy();
  108. expect(Array.isArray(r.data.records)).toBeTruthy();
  109. expect(r.data).toHaveProperty('total');
  110. });
  111. // ===== 场景5:五维能量概览 =====
  112. test('[场景5] 获取五维能量概览 - 期望返回五维汇总数据', async ({ request }) => {
  113. const r = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, ctx.token);
  114. expect(r.code).toBe(200);
  115. expect(r.data).toBeTruthy();
  116. // 五维:身/心/智/行/富 → body / mind / wisdom / action / wealth
  117. const dims = (r.data && r.data.dimensions) || [];
  118. expect(Array.isArray(dims)).toBeTruthy();
  119. const codes = dims.map((d) => d.code);
  120. for (const c of ['body', 'mind', 'wisdom', 'action', 'wealth']) {
  121. expect(codes, '五维概览应包含维度 ' + c).toContain(c);
  122. }
  123. });
  124. // ===== 场景6:完整能量收支流程 =====
  125. test('[场景6] 完整能量收支流程 - 期望全流程成功', async ({ request }) => {
  126. test.fixme(true, 'CFC-EN-001:/api/energy/grant 返回 500 服务器内部错误,修复后移除本标记');
  127. const taskId = Number(uniqueSuffix());
  128. const amount = 5;
  129. // 1) 发放
  130. const grant = await apiPost(
  131. request,
  132. '/api/energy/grant',
  133. { memberId: selfMemberId, taskId, amount, dimensionCode: DIMENSION },
  134. ctx.token
  135. );
  136. expect(grant.code).toBe(200);
  137. // 2) 流水可查
  138. const logs = await apiPost(request, '/api/energy/logs', { memberId: selfMemberId }, ctx.token);
  139. expect(logs.code).toBe(200);
  140. expect(logs.data.records.some((r) => String(r.sourceId) === String(taskId))).toBeTruthy();
  141. // 3) 概览可查
  142. const overview = await apiPost(request, '/api/energy/overview', { memberId: selfMemberId }, ctx.token);
  143. expect(overview.code).toBe(200);
  144. });
  145. });