wish-exchange-flow.spec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /**
  2. * ================================================================
  3. * 场景:心愿兑换审批全流程 E2E 测试
  4. * ================================================================
  5. * 用户故事:作为孩子,我希望创建心愿并通过积累积分兑换,
  6. * 以获得家长的认可和实际的奖励物品。
  7. *
  8. * 流程步骤:
  9. * 1. 孩子创建心愿(如"想要一套科学实验套装")
  10. * 2. 家长查看心愿并设定所需积分(如 500 积分)
  11. * 3. 孩子通过完成任务积累积分后,申请兑换心愿
  12. * 4. 家长审批通过,系统自动扣除积分
  13. * 5. 家长购买物品后标记"已购买"
  14. * 6. 孩子确认收货,心愿流程完成
  15. *
  16. * 关键里程碑:
  17. * - [Milestone-1] 心愿创建成功,获得心愿ID
  18. * - [Milestone-2] 家长定价成功,心愿状态变更为"已定价"
  19. * - [Milestone-3] 兑换申请提交,状态变更为"兑换中"
  20. * - [Milestone-4] 家长审批通过,积分立即扣减
  21. * - [Milestone-5] 标记已购买,等待孩子确认
  22. * - [Milestone-6] 孩子确认收货,心愿完成
  23. * ================================================================
  24. *
  25. * 运行:npx playwright test tests/e2e/wish-exchange-flow.spec.js
  26. */
  27. const { test, expect } = require('@playwright/test');
  28. test.describe('【场景流程】心愿兑换审批全流程', () => {
  29. // ===== 场景1:孩子创建心愿 =====
  30. /**
  31. * 场景:孩子创建一个新的心愿
  32. * 角色:孩子
  33. * 触发条件:孩子点击"创建心愿"并填写表单
  34. */
  35. test('[场景1] 孩子创建心愿 - 期望心愿创建成功', async ({ page }) => {
  36. // ========== Given:进入创建心愿页面 ==========
  37. await page.goto('/#/pages/wishes/create');
  38. await page.waitForLoadState('networkidle');
  39. // ========== When:填写心愿信息 ==========
  40. // 输入心愿标题
  41. const titleInput = page.locator('input[placeholder*="心愿"], .wish-title-input, input[name="title"]');
  42. if (await titleInput.isVisible()) {
  43. await titleInput.fill('科学实验套装');
  44. }
  45. // 输入心愿描述
  46. const descInput = page.locator('textarea[placeholder*="描述"], .wish-desc-input');
  47. if (await descInput.isVisible()) {
  48. await descInput.fill('包含50个实验的科学实验套装,寓教于乐');
  49. }
  50. // 输入预估价格
  51. const priceInput = page.locator('input[placeholder*="价格"], .wish-price-input, input[name="estimatedPrice"]');
  52. if (await priceInput.isVisible()) {
  53. await priceInput.fill('299');
  54. }
  55. // ========== Then:提交创建 ==========
  56. await page.click('.submit-btn, .create-btn');
  57. // ========== Then:Milestone-1 - 心愿创建成功 ========
  58. await page.waitForSelector('.success-tip, .wish-created, .create-success', { timeout: 10000 });
  59. // 验证心愿创建成功
  60. const successTip = page.locator('.success-tip, .wish-created, .create-success');
  61. expect(await successTip.isVisible()).toBeTruthy();
  62. });
  63. // ===== 场景2:家长定价 =====
  64. /**
  65. * 场景:家长为孩子的心愿设定所需积分
  66. * 角色:家长
  67. * 触发条件:家长打开心愿详情,决定设定多少积分可兑换
  68. */
  69. test('[场景2] 家长设定心愿积分 - 期望积分设定成功且状态更新', async ({ page }) => {
  70. // ========== Given:进入待定价心愿列表 ==========
  71. await page.goto('/#/pages/wishes/pending');
  72. await page.waitForLoadState('networkidle');
  73. // ========== Step 1:找到待定价心愿 ==========
  74. await page.waitForSelector('.wish-item', { timeout: 10000 });
  75. const pendingWish = page.locator('.wish-item:has-text("待定价"), .wish-item:has-text("待设置")').first();
  76. await pendingWish.click();
  77. // ========== Step 2:设定积分值 ==========
  78. await page.waitForSelector('.price-input, .points-input', { timeout: 5000 });
  79. const priceInput = page.locator('.price-input, .points-input, input[name="pointsRequired"]');
  80. await priceInput.fill('500');
  81. // ========== When:提交定价 ==========
  82. await page.click('.set-price-btn, .confirm-price-btn, .submit-btn');
  83. // ========== Then:Milestone-2 - 定价成功 ========
  84. await page.waitForSelector('.success-tip, .price-set, .set-success', { timeout: 10000 });
  85. // 验证状态变更
  86. const wishStatus = page.locator('.wish-status, .status-tag');
  87. if (await wishStatus.isVisible()) {
  88. const statusText = await wishStatus.textContent();
  89. expect(statusText).toMatch(/已定价|待兑换/);
  90. }
  91. });
  92. /**
  93. * 场景:家长设定的积分值为负数或零
  94. * 角色:家长
  95. * 触发条件:表单校验未通过
  96. */
  97. test('[场景2b] 家长设定无效积分值 - 期望返回错误提示', async ({ page }) => {
  98. // ========== Given:进入心愿编辑页面 ==========
  99. await page.goto('/#/pages/wishes/edit/1');
  100. await page.waitForLoadState('networkidle');
  101. // ========== When:设定无效积分值 ==========
  102. const priceInput = page.locator('.price-input, .points-input, input[name="pointsRequired"]');
  103. if (await priceInput.isVisible()) {
  104. await priceInput.fill('0');
  105. }
  106. await page.click('.submit-btn');
  107. // ========== Then:返回错误提示 ========
  108. await page.waitForSelector('.error-tip, .van-field__error, .form-error', { timeout: 5000 });
  109. const errorTip = page.locator('.error-tip, .van-field__error, .form-error');
  110. expect(await errorTip.isVisible()).toBeTruthy();
  111. });
  112. // ===== 场景3:孩子申请兑换 =====
  113. /**
  114. * 场景:孩子积累足够积分后,申请兑换心愿
  115. * 角色:孩子
  116. * 触发条件:孩子点击心愿的"申请兑换"按钮
  117. */
  118. test('[场景3] 孩子申请兑换心愿 - 期望兑换申请提交成功', async ({ page }) => {
  119. // ========== Given:进入我的心愿列表 ==========
  120. await page.goto('/#/pages/wishes/my');
  121. await page.waitForLoadState('networkidle');
  122. // ========== Step 1:找到已定价心愿 ==========
  123. await page.waitForSelector('.wish-item', { timeout: 10000 });
  124. const pricedWish = page.locator('.wish-item:has-text("已定价"), .wish-item:has-text("可兑换")').first();
  125. await pricedWish.click();
  126. // ========== Step 2:点击兑换按钮 ==========
  127. await page.waitForSelector('.exchange-btn, .apply-exchange-btn', { timeout: 5000 });
  128. await page.click('.exchange-btn, .apply-exchange-btn');
  129. // ========== Then:Milestone-3 - 兑换申请提交成功 ========
  130. await page.waitForSelector('.success-tip, .exchange-applied, .apply-success', { timeout: 10000 });
  131. const successTip = page.locator('.success-tip, .exchange-applied, .apply-success');
  132. expect(await successTip.isVisible()).toBeTruthy();
  133. });
  134. /**
  135. * 场景:孩子积分不足时尝试兑换
  136. * 角色:孩子
  137. * 触发条件:孩子积分不足以兑换该心愿
  138. */
  139. test('[场景3b] 积分不足时申请兑换 - 期望返回错误提示', async ({ page }) => {
  140. // ========== Given:进入心愿详情 ==========
  141. await page.goto('/#/pages/wishes/detail/1');
  142. await page.waitForLoadState('networkidle');
  143. // ========== When:点击兑换 ==========
  144. const exchangeBtn = page.locator('.exchange-btn, .apply-exchange-btn');
  145. if (await exchangeBtn.isVisible()) {
  146. await exchangeBtn.click();
  147. // ========== Then:返回积分不足提示 ========
  148. await page.waitForSelector('.error-tip, .insufficient-points, .points-error', { timeout: 5000 });
  149. const errorTip = page.locator('.error-tip, .insufficient-points, .points-error');
  150. expect(await errorTip.isVisible()).toBeTruthy();
  151. // 验证错误信息包含积分相关字样
  152. const errorText = await errorTip.textContent();
  153. expect(errorText).toMatch(/积分|不足/);
  154. }
  155. });
  156. // ===== 场景4:家长审批兑换 =====
  157. /**
  158. * 场景:家长审批通过孩子的兑换申请,积分立即扣除
  159. * 角色:家长
  160. * 触发条件:家长查看兑换申请并点击"批准"
  161. */
  162. test('[场景4] 家长审批通过兑换 - 期望审批成功且积分扣除', async ({ page }) => {
  163. // ========== Given:进入待审批兑换列表 ==========
  164. await page.goto('/#/pages/wishes/pending-approval');
  165. await page.waitForLoadState('networkidle');
  166. // ========== Step 1:找到待审批的兑换申请 ==========
  167. await page.waitForSelector('.exchange-item, .approval-item', { timeout: 10000 });
  168. const pendingApproval = page.locator('.exchange-item:has-text("申请中"), .approval-item:has-text("待审批")').first();
  169. await pendingApproval.click();
  170. // ========== Step 2:查看积分信息 ==========
  171. await page.waitForSelector('.points-required, .wish-points', { timeout: 5000 });
  172. // ========== When:点击批准 ==========
  173. await page.click('.approve-btn, .confirm-btn, .approve-exchange-btn');
  174. // ========== Then:Milestone-4 - 审批通过 + 积分扣减 ========
  175. await page.waitForSelector('.success-tip, .approved, .approve-success', { timeout: 10000 });
  176. // 验证积分已扣减(如果有显示)
  177. const remainingPoints = page.locator('.remaining-points, .current-points');
  178. if (await remainingPoints.isVisible()) {
  179. const pointsText = await remainingPoints.textContent();
  180. expect(pointsText).toMatch(/\d+/);
  181. }
  182. });
  183. /**
  184. * 场景:家长拒绝孩子的兑换申请
  185. * 角色:家长
  186. * 触发条件:家长认为该心愿不合适,点击"拒绝"
  187. */
  188. test('[场景4b] 家长拒绝兑换申请 - 期望心愿状态回退', async ({ page }) => {
  189. // ========== Given:进入待审批兑换详情 ==========
  190. await page.goto('/#/pages/wishes/approval/1');
  191. await page.waitForLoadState('networkidle');
  192. // ========== When:点击拒绝 ==========
  193. await page.click('.reject-btn, .reject-exchange-btn');
  194. // 填写拒绝原因
  195. const reasonInput = page.locator('textarea[placeholder*="原因"], .reject-reason-input');
  196. if (await reasonInput.isVisible()) {
  197. await reasonInput.fill('本月预算已用完,下个月再兑换好吗?');
  198. }
  199. // 确认拒绝
  200. await page.click('.confirm-reject-btn, .submit-btn');
  201. // ========== Then:拒绝成功,心愿状态回退 ========
  202. await page.waitForSelector('.rejected, .reject-success', { timeout: 10000 });
  203. // 验证心愿状态回退到"已定价"
  204. const wishStatus = page.locator('.wish-status, .status-tag');
  205. if (await wishStatus.isVisible()) {
  206. const statusText = await wishStatus.textContent();
  207. expect(statusText).toMatch(/已定价|待兑换/);
  208. }
  209. });
  210. // ===== 场景5:标记已购买 =====
  211. /**
  212. * 场景:家长购买物品后标记心愿为"已购买"
  213. * 角色:家长
  214. * 触发条件:家长完成购买操作
  215. */
  216. test('[场景5] 家长标记心愿已购买 - 期望状态变更为待收货', async ({ page }) => {
  217. // ========== Given:进入已批准兑换列表 ==========
  218. await page.goto('/#/pages/wishes/approved');
  219. await page.waitForLoadState('networkidle');
  220. // ========== Step 1:找到已批准的兑换 ==========
  221. await page.waitForSelector('.exchange-item, .approved-item', { timeout: 10000 });
  222. const approvedItem = page.locator('.exchange-item:has-text("已批准"), .approved-item').first();
  223. await approvedItem.click();
  224. // ========== When:点击"已购买" ==========
  225. await page.click('.mark-purchased-btn, .fulfill-btn');
  226. // ========== Then:Milestone-5 - 标记成功 ========
  227. await page.waitForSelector('.success-tip, .purchased, .fulfill-success', { timeout: 10000 });
  228. // 验证状态变更为"待收货"
  229. const wishStatus = page.locator('.wish-status, .status-tag');
  230. if (await wishStatus.isVisible()) {
  231. const statusText = await wishStatus.textContent();
  232. expect(statusText).toMatch(/待收货|已购买/);
  233. }
  234. });
  235. // ===== 场景6:孩子确认收货 =====
  236. /**
  237. * 场景:孩子收到物品后确认收货,心愿流程完成
  238. * 角色:孩子
  239. * 触发条件:孩子收到实物并确认无误
  240. */
  241. test('[场景6] 孩子确认收货 - 期望心愿流程完成', async ({ page }) => {
  242. // ========== Given:进入待确认收货列表 ==========
  243. await page.goto('/#/pages/wishes/to-confirm');
  244. await page.waitForLoadState('networkidle');
  245. // ========== Step 1:找到待确认的心愿 ==========
  246. await page.waitForSelector('.wish-item', { timeout: 10000 });
  247. const toConfirm = page.locator('.wish-item:has-text("待收货"), .wish-item:has-text("已购买")').first();
  248. await toConfirm.click();
  249. // ========== When:点击"确认收货" ==========
  250. await page.click('.confirm-receive-btn, .confirm-btn');
  251. // ========== Then:Milestone-6 - 心愿完成 ========
  252. await page.waitForSelector('.success-tip, .completed, .confirm-success', { timeout: 10000 });
  253. // 验证状态变更为"已完成"
  254. const wishStatus = page.locator('.wish-status, .status-tag');
  255. if (await wishStatus.isVisible()) {
  256. const statusText = await wishStatus.textContent();
  257. expect(statusText).toMatch(/已完成|已收货/);
  258. }
  259. });
  260. // ===== 场景7:完整心愿流程(端到端)=====
  261. test('[场景7] 完整心愿生命周期 - 期望全流程成功', async ({ page }) => {
  262. // ========== Step 1:孩子创建心愿 ==========
  263. await page.goto('/#/pages/wishes/create');
  264. await page.waitForLoadState('networkidle');
  265. const titleInput = page.locator('input[name="title"], .wish-title-input');
  266. if (await titleInput.isVisible()) {
  267. await titleInput.fill('科学杂志订阅_' + Date.now());
  268. }
  269. const descInput = page.locator('textarea[name="description"], .wish-desc-input');
  270. if (await descInput.isVisible()) {
  271. await descInput.fill('儿童科学爱好者订阅一年');
  272. }
  273. await page.click('.submit-btn');
  274. await page.waitForSelector('.success-tip, .wish-created', { timeout: 10000 });
  275. // ========== Step 2:家长定价 ==========
  276. await page.goto('/#/pages/wishes/pending');
  277. await page.waitForLoadState('networkidle');
  278. await page.waitForSelector('.wish-item', { timeout: 10000 });
  279. const wishItem = page.locator('.wish-item').first();
  280. const editBtn = wishItem.locator('.edit-btn, .price-btn');
  281. if (await editBtn.isVisible()) {
  282. await editBtn.click();
  283. await page.waitForSelector('.price-input', { timeout: 5000 });
  284. const priceInput = page.locator('.price-input, input[name="pointsRequired"]');
  285. await priceInput.fill('100');
  286. await page.click('.set-price-btn');
  287. await page.waitForSelector('.success-tip', { timeout: 10000 });
  288. }
  289. // ========== Step 3:孩子申请兑换 ==========
  290. await page.goto('/#/pages/wishes/my');
  291. await page.waitForLoadState('networkidle');
  292. await page.waitForSelector('.wish-item', { timeout: 10000 });
  293. const exchangeBtn = page.locator('.exchange-btn').first();
  294. if (await exchangeBtn.isVisible()) {
  295. await exchangeBtn.click();
  296. await page.waitForSelector('.success-tip, .exchange-applied', { timeout: 10000 });
  297. }
  298. // ========== Step 4:家长审批通过 ==========
  299. await page.goto('/#/pages/wishes/pending-approval');
  300. await page.waitForLoadState('networkidle');
  301. const approvalItem = page.locator('.exchange-item, .approval-item').first();
  302. if (await approvalItem.isVisible()) {
  303. await approvalItem.click();
  304. await page.waitForSelector('.approve-btn', { timeout: 5000 });
  305. await page.click('.approve-btn');
  306. await page.waitForSelector('.success-tip, .approved', { timeout: 10000 });
  307. }
  308. // ========== Step 5:家长标记已购买 ==========
  309. await page.goto('/#/pages/wishes/approved');
  310. await page.waitForLoadState('networkidle');
  311. const purchasedBtn = page.locator('.mark-purchased-btn, .fulfill-btn').first();
  312. if (await purchasedBtn.isVisible()) {
  313. await purchasedBtn.click();
  314. await page.waitForSelector('.success-tip, .purchased', { timeout: 10000 });
  315. }
  316. // ========== Step 6:孩子确认收货 ==========
  317. await page.goto('/#/pages/wishes/to-confirm');
  318. await page.waitForLoadState('networkidle');
  319. const confirmBtn = page.locator('.confirm-receive-btn').first();
  320. if (await confirmBtn.isVisible()) {
  321. await confirmBtn.click();
  322. await page.waitForSelector('.success-tip, .completed', { timeout: 10000 });
  323. }
  324. // 验证最终状态
  325. const finalStatus = page.locator('.wish-status');
  326. if (await finalStatus.isVisible()) {
  327. const statusText = await finalStatus.textContent();
  328. expect(statusText).toMatch(/已完成/);
  329. }
  330. });
  331. // ===== 场景8:家长拒绝心愿 =====
  332. test('[场景8] 家长直接拒绝心愿 - 期望心愿被关闭', async ({ page }) => {
  333. // ========== Given:进入待处理心愿 ==========
  334. await page.goto('/#/pages/wishes/pending');
  335. await page.waitForLoadState('networkidle');
  336. // ========== Step 1:选择心愿 ==========
  337. await page.waitForSelector('.wish-item', { timeout: 10000 });
  338. const wishItem = page.locator('.wish-item').first();
  339. await wishItem.click();
  340. // ========== When:点击拒绝 ==========
  341. await page.waitForSelector('.reject-btn, .refuse-btn', { timeout: 5000 });
  342. await page.click('.reject-btn');
  343. // 填写原因
  344. const reasonInput = page.locator('textarea[name="reason"], .reject-reason-input');
  345. if (await reasonInput.isVisible()) {
  346. await reasonInput.fill('这套书家里已经有了,换个其他的吧');
  347. }
  348. await page.click('.confirm-reject-btn');
  349. // ========== Then:心愿被关闭 ========
  350. await page.waitForSelector('.rejected, .reject-success', { timeout: 10000 });
  351. const wishStatus = page.locator('.wish-status, .status-tag');
  352. if (await wishStatus.isVisible()) {
  353. const statusText = await wishStatus.textContent();
  354. expect(statusText).toMatch(/已拒绝|已关闭/);
  355. }
  356. });
  357. // ===== 场景9:孩子取消心愿 =====
  358. test('[场景9] 孩子取消心愿 - 期望心愿被取消', async ({ page }) => {
  359. // ========== Given:进入我的心愿 ==========
  360. await page.goto('/#/pages/wishes/my');
  361. await page.waitForLoadState('networkidle');
  362. // ========== Step 1:选择心愿 ==========
  363. await page.waitForSelector('.wish-item', { timeout: 10000 });
  364. const wishItem = page.locator('.wish-item').first();
  365. // 点击取消按钮
  366. const cancelBtn = wishItem.locator('.cancel-btn, .cancel-wish-btn');
  367. if (await cancelBtn.isVisible()) {
  368. await cancelBtn.click();
  369. // 确认取消
  370. await page.waitForSelector('.confirm-dialog, .van-dialog', { timeout: 5000 });
  371. await page.click('.confirm-dialog .van-button--primary, .confirm-cancel-btn');
  372. // ========== Then:心愿被取消 ========
  373. await page.waitForTimeout(1000);
  374. // 验证心愿不在列表中或状态变更
  375. const statusTag = wishItem.locator('.status-tag');
  376. if (await statusTag.isVisible()) {
  377. const statusText = await statusTag.textContent();
  378. expect(statusText).toMatch(/已取消|已关闭/);
  379. }
  380. }
  381. });
  382. });