daily-checkin-streak.spec.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /**
  2. * ================================================================
  3. * 场景:连续签到奖励全流程 E2E 测试
  4. * ================================================================
  5. * 用户故事:作为孩子,我希望每天完成健康打卡,
  6. * 保持连续签到记录以获得更多奖励。
  7. *
  8. * 流程步骤:
  9. * 1. 孩子每天完成健康打卡(饮食、运动、睡眠、饮水、心情)
  10. * 2. 系统记录打卡并更新连续签到天数
  11. * 3. 达到特定天数阈值时发放奖励(积分或能量)
  12. * 4. 第二天继续打卡以保持连续
  13. * 5. 如果中断,系统重置连续天数为0
  14. *
  15. * 关键里程碑:
  16. * - [Milestone-1] 打卡记录创建成功
  17. * - [Milestone-2] 连续签到天数正确累计
  18. * - [Milestone-3] 达到奖励阈值,奖励发放
  19. * - [Milestone-4] 中断后重置连续天数
  20. * ================================================================
  21. *
  22. * 运行:npx playwright test tests/e2e/daily-checkin-streak.spec.js
  23. */
  24. const { test, expect } = require('@playwright/test');
  25. test.describe('【场景流程】连续签到奖励全流程', () => {
  26. // ===== 场景1:每日健康打卡 =====
  27. /**
  28. * 场景:孩子完成每日健康打卡
  29. * 角色:孩子
  30. * 触发条件:每天进入打卡页面填写健康数据
  31. */
  32. test('[场景1] 孩子完成每日打卡 - 期望打卡成功', async ({ page }) => {
  33. // ========== Given:进入健康打卡页面 ==========
  34. await page.goto('/#/pages/health/checkin');
  35. await page.waitForLoadState('networkidle');
  36. // ========== Step 1:选择孩子(如需要)==========
  37. const childPicker = page.locator('.child-picker');
  38. if (await childPicker.isVisible()) {
  39. await childPicker.click();
  40. await page.waitForSelector('.child-option', { timeout: 5000 });
  41. await page.click('.child-option:first-child');
  42. }
  43. // ========== Step 2:填写饮食分数 ==========
  44. const dietScore = page.locator('.diet-score-input, input[name="dietScore"]');
  45. if (await dietScore.isVisible()) {
  46. await dietScore.fill('85');
  47. } else {
  48. // 使用滑动条或评分组件
  49. const dietSlider = page.locator('.diet-score .slider');
  50. if (await dietSlider.isVisible()) {
  51. await dietSlider.evaluate(el => el.value = 85);
  52. }
  53. }
  54. // ========== Step 3:填写运动分数 ==========
  55. const exerciseScore = page.locator('.exercise-score-input, input[name="exerciseScore"]');
  56. if (await exerciseScore.isVisible()) {
  57. await exerciseScore.fill('90');
  58. }
  59. // ========== Step 4:填写睡眠分数 ==========
  60. const sleepScore = page.locator('.sleep-score-input, input[name="sleepScore"]');
  61. if (await sleepScore.isVisible()) {
  62. await sleepScore.fill('80');
  63. }
  64. // ========== Step 5:填写饮水量 ==========
  65. const waterIntake = page.locator('.water-intake-input, input[name="waterIntake"]');
  66. if (await waterIntake.isVisible()) {
  67. await waterIntake.fill('2000');
  68. }
  69. // ========== Step 6:填写心情分数 ==========
  70. const moodScore = page.locator('.mood-score-input, input[name="moodScore"]');
  71. if (await moodScore.isVisible()) {
  72. await moodScore.fill('88');
  73. }
  74. // ========== When:提交打卡 ==========
  75. await page.click('.submit-btn, .checkin-btn, .save-btn');
  76. // ========== Then:Milestone-1 - 打卡成功 ========
  77. await page.waitForSelector('.success-tip, .checkin-success, .submitted', { timeout: 10000 });
  78. // 验证打卡成功提示
  79. const successTip = page.locator('.success-tip, .checkin-success');
  80. expect(await successTip.isVisible()).toBeTruthy();
  81. });
  82. // ===== 场景2:查看打卡列表 =====
  83. /**
  84. * 场景:查看历史打卡记录
  85. * 角色:孩子或家长
  86. * 触发条件:进入打卡历史页面
  87. */
  88. test('[场景2] 查看打卡记录列表 - 期望返回历史记录', async ({ page }) => {
  89. // ========== Given:进入打卡记录页面 ==========
  90. await page.goto('/#/pages/health/checkin-list');
  91. await page.waitForLoadState('networkidle');
  92. // ========== Step 1:选择孩子 ==========
  93. const childPicker = page.locator('.child-picker');
  94. if (await childPicker.isVisible()) {
  95. await childPicker.click();
  96. await page.waitForSelector('.child-option', { timeout: 5000 });
  97. await page.click('.child-option:first-child');
  98. }
  99. // ========== When:查看打卡列表 ==========
  100. await page.waitForSelector('.checkin-item, .record-item', { timeout: 10000 });
  101. // ========== Then:返回历史记录 ========
  102. const recordItems = page.locator('.checkin-item, .record-item');
  103. const count = await recordItems.count();
  104. expect(count).toBeGreaterThan(0);
  105. // 验证日期显示
  106. const firstRecord = recordItems.first();
  107. const recordDate = firstRecord.locator('.record-date, .date-text');
  108. expect(await recordDate.isVisible()).toBeTruthy();
  109. });
  110. // ===== 场景3:获取连续签到进度 =====
  111. /**
  112. * 场景:孩子查看自己的连续签到进度
  113. * 角色:孩子
  114. * 触发条件:查看签到激励页面
  115. */
  116. test('[场景3] 获取连续签到进度 - 期望返回当前连续天数和奖励状态', async ({ page }) => {
  117. // ========== Given:进入签到进度页面 ==========
  118. await page.goto('/#/pages/streak/progress');
  119. await page.waitForLoadState('networkidle');
  120. // ========== When:页面加载完成 ==========
  121. await page.waitForSelector('.streak-progress, .progress-container', { timeout: 10000 });
  122. // ========== Then:Milestone-2 - 返回连续天数等信息 ========
  123. // 验证当前连续天数
  124. const currentStreak = page.locator('.current-streak, .streak-count');
  125. expect(await currentStreak.isVisible()).toBeTruthy();
  126. const streakText = await currentStreak.textContent();
  127. expect(streakText).toMatch(/\d+/);
  128. // 验证最长连续天数
  129. const longestStreak = page.locator('.longest-streak, .best-streak');
  130. if (await longestStreak.isVisible()) {
  131. const bestText = await longestStreak.textContent();
  132. expect(bestText).toMatch(/\d+/);
  133. }
  134. // 验证今日打卡状态
  135. const todayChecked = page.locator('.today-checked, .today-status');
  136. if (await todayChecked.isVisible()) {
  137. const statusText = await todayChecked.textContent();
  138. expect(statusText).toMatch(/已打卡|未打卡/);
  139. }
  140. });
  141. // ===== 场景4:达到奖励阈值发放奖励 =====
  142. /**
  143. * 场景:孩子连续签到达到7天阈值,系统发放奖励
  144. * 角色:系统(自动触发)
  145. * 触发条件:连续签到天数达到特定阈值
  146. */
  147. test('[场景4] 连续7天打卡达成 - 期望奖励发放', async ({ page }) => {
  148. // ========== Given:已达到7天连续签到 ==========
  149. await page.goto('/#/pages/streak/reward');
  150. await page.waitForLoadState('networkidle');
  151. // ========== When:查看奖励状态 ==========
  152. await page.waitForSelector('.reward-info, .reward-container', { timeout: 10000 });
  153. // ========== Then:Milestone-3 - 奖励发放或可领取 ========
  154. // 检查是否有可领取奖励
  155. const rewardBadge = page.locator('.reward-badge, .pending-reward');
  156. const rewardClaimBtn = page.locator('.claim-reward-btn, .get-reward-btn');
  157. if (await rewardBadge.isVisible() || await rewardClaimBtn.isVisible()) {
  158. // 有待领取奖励
  159. expect(true).toBeTruthy();
  160. // 可以点击领取(如果是待领取状态)
  161. if (await rewardClaimBtn.isVisible()) {
  162. await rewardClaimBtn.click();
  163. await page.waitForSelector('.reward-claimed, .claim-success', { timeout: 10000 });
  164. }
  165. } else {
  166. // 奖励已发放,检查发放记录
  167. const rewardRecord = page.locator('.reward-record, .reward-item');
  168. expect(await rewardRecord.isVisible()).toBeTruthy();
  169. }
  170. });
  171. /**
  172. * 场景:孩子连续签到达到30天,获得大奖励
  173. * 角色:系统
  174. * 触发条件:连续签到达到30天
  175. */
  176. test('[场景4b] 连续30天打卡达成 - 期望获得大额奖励', async ({ page }) => {
  177. // ========== Given:已达到30天连续签到 ==========
  178. await page.goto('/#/pages/streak/reward-detail?days=30');
  179. await page.waitForLoadState('networkidle');
  180. // ========== When:查看30天奖励 ==========
  181. await page.waitForSelector('.big-reward, .reward-detail', { timeout: 10000 });
  182. // ========== Then:奖励发放 ========
  183. // 验证大额奖励显示
  184. const bigReward = page.locator('.big-reward, .reward-amount');
  185. expect(await bigReward.isVisible()).toBeTruthy();
  186. // 验证奖励领取状态
  187. const rewardStatus = page.locator('.reward-status, .status');
  188. if (await rewardStatus.isVisible()) {
  189. const statusText = await rewardStatus.textContent();
  190. expect(statusText).toMatch(/已发放|已领取|待领取/);
  191. }
  192. });
  193. // ===== 场景5:打卡中断重置 =====
  194. /**
  195. * 场景:孩子忘记打卡,连续签到中断
  196. * 角色:系统(定时任务触发)
  197. * 触发条件:凌晨检查发现昨天未打卡
  198. */
  199. test('[场景5] 打卡中断 - 期望连续天数重置', async ({ page }) => {
  200. // ========== Given:进入签到进度页面 ==========
  201. await page.goto('/#/pages/streak/progress');
  202. await page.waitForLoadState('networkidle');
  203. // ========== When:系统重置中断的签到 ==========
  204. // 等待系统检查并更新状态
  205. await page.waitForTimeout(2000);
  206. // 查看当前连续天数显示
  207. const currentStreak = page.locator('.current-streak, .streak-count');
  208. if (await currentStreak.isVisible()) {
  209. const streakText = await currentStreak.textContent();
  210. const streakValue = parseInt(streakText.replace(/\D/g, '')) || 0;
  211. // 如果昨天未打卡,连续天数应该被重置
  212. // 注意:这里需要结合实际情况验证
  213. // 可能的显示:当前连续天数为1或0(取决于系统设计)
  214. expect(streakValue).toBeLessThanOrEqual(1);
  215. }
  216. // ========== Then:Milestone-4 - 连续天数重置 ========
  217. // 验证重置提示(如有)
  218. const resetTip = page.locator('.streak-reset, .reset-notice');
  219. // 可能显示"已重置"或"重新开始"等提示
  220. });
  221. // ===== 场景6:更新打卡记录 =====
  222. /**
  223. * 场景:孩子补充或修改今天的打卡数据
  224. * 角色:孩子
  225. * 触发条件:发现打卡数据填错需要修改
  226. */
  227. test('[场景6] 更新打卡记录 - 期望更新成功', async ({ page }) => {
  228. // ========== Given:进入打卡详情 ==========
  229. await page.goto('/#/pages/health/checkin-detail/1');
  230. await page.waitForLoadState('networkidle');
  231. // ========== When:修改打卡数据 ==========
  232. const editBtn = page.locator('.edit-btn, .modify-btn');
  233. if (await editBtn.isVisible()) {
  234. await editBtn.click();
  235. await page.waitForSelector('.edit-form, .checkin-form', { timeout: 5000 });
  236. // 修改饮食分数
  237. const dietScore = page.locator('input[name="dietScore"], .diet-score-input');
  238. if (await dietScore.isVisible()) {
  239. await dietScore.clear();
  240. await dietScore.fill('90');
  241. }
  242. // 保存修改
  243. await page.click('.save-btn, .submit-btn');
  244. // ========== Then:更新成功 ========
  245. await page.waitForSelector('.success-tip, .update-success', { timeout: 10000 });
  246. // 验证修改后的数据显示
  247. const updatedScore = page.locator('.diet-score-value, .updated-diet');
  248. if (await updatedScore.isVisible()) {
  249. const scoreText = await updatedScore.textContent();
  250. expect(scoreText).toMatch(/90/);
  251. }
  252. }
  253. });
  254. // ===== 场景7:删除打卡记录 =====
  255. /**
  256. * 场景:孩子删除一条错误的打卡记录
  257. * 角色:孩子
  258. * 触发条件:发现打卡记录有误需要删除
  259. */
  260. test('[场景7] 删除打卡记录 - 期望删除成功', async ({ page }) => {
  261. // ========== Given:进入打卡列表 ==========
  262. await page.goto('/#/pages/health/checkin-list');
  263. await page.waitForLoadState('networkidle');
  264. // 获取删除前的记录数
  265. await page.waitForSelector('.checkin-item', { timeout: 10000 });
  266. const beforeCount = await page.locator('.checkin-item').count();
  267. // ========== Step 1:找到要删除的记录 ==========
  268. const lastItem = page.locator('.checkin-item').last();
  269. const deleteBtn = lastItem.locator('.delete-btn, .btn-delete');
  270. if (await deleteBtn.isVisible()) {
  271. await deleteBtn.click();
  272. // 确认删除
  273. await page.waitForSelector('.confirm-dialog, .van-dialog', { timeout: 5000 });
  274. await page.click('.confirm-dialog .van-button--primary, .confirm-delete');
  275. // ========== Then:删除成功 ========
  276. await page.waitForTimeout(1000);
  277. // 验证记录数减少
  278. const afterCount = await page.locator('.checkin-item').count();
  279. expect(afterCount).toBeLessThan(beforeCount);
  280. }
  281. });
  282. // ===== 场景8:完整连续签到流程(端到端)=====
  283. test('[场景8] 完整连续签到流程 - 期望全流程成功', async ({ page }) => {
  284. // ========== Step 1:查看签到进度 ==========
  285. await page.goto('/#/pages/streak/progress');
  286. await page.waitForLoadState('networkidle');
  287. await page.waitForSelector('.streak-progress', { timeout: 10000 });
  288. // 记录当前连续天数
  289. const currentStreakEl = page.locator('.current-streak');
  290. let beforeStreak = 0;
  291. if (await currentStreakEl.isVisible()) {
  292. beforeStreak = parseInt(await currentStreakEl.textContent()) || 0;
  293. }
  294. // ========== Step 2:进行每日打卡 ==========
  295. await page.goto('/#/pages/health/checkin');
  296. await page.waitForLoadState('networkidle');
  297. // 选择孩子
  298. const childPicker = page.locator('.child-picker');
  299. if (await childPicker.isVisible()) {
  300. await childPicker.click();
  301. await page.waitForSelector('.child-option', { timeout: 5000 });
  302. await page.click('.child-option:first-child');
  303. }
  304. // 填写各项分数
  305. const inputs = ['diet', 'exercise', 'sleep', 'mood'];
  306. for (const type of inputs) {
  307. const input = page.locator(`.${type}-score-input, input[name="${type}Score"]`);
  308. if (await input.isVisible()) {
  309. await input.fill('85');
  310. }
  311. }
  312. // 填写饮水量
  313. const waterInput = page.locator('.water-intake-input, input[name="waterIntake"]');
  314. if (await waterInput.isVisible()) {
  315. await waterInput.fill('2000');
  316. }
  317. await page.click('.submit-btn');
  318. await page.waitForSelector('.success-tip, .checkin-success', { timeout: 10000 });
  319. // ========== Step 3:再次查看签到进度 ==========
  320. await page.goto('/#/pages/streak/progress');
  321. await page.waitForLoadState('networkidle');
  322. // 验证连续天数增加
  323. await page.waitForSelector('.current-streak', { timeout: 10000 });
  324. const afterStreakEl = page.locator('.current-streak');
  325. if (await afterStreakEl.isVisible()) {
  326. const afterStreak = parseInt(await afterStreakEl.textContent()) || 0;
  327. // 连续天数应该增加(除非已经是最大值或今日已打过卡)
  328. expect(afterStreak).toBeGreaterThanOrEqual(beforeStreak);
  329. }
  330. // ========== Step 4:查看打卡记录列表 ==========
  331. await page.goto('/#/pages/health/checkin-list');
  332. await page.waitForLoadState('networkidle');
  333. await page.waitForSelector('.checkin-item', { timeout: 10000 });
  334. // 验证今日打卡记录存在
  335. const todayRecord = page.locator('.checkin-item').first();
  336. expect(await todayRecord.isVisible()).toBeTruthy();
  337. });
  338. });