planner-bind-invite.spec.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /**
  2. * ================================================================
  3. * 场景:规划师绑定邀请全流程 E2E 测试
  4. * ================================================================
  5. * 用户故事:作为成长规划师,我希望邀请家长加入我的服务团队,
  6. * 以便建立长期服务关系并管理我的客户家庭。
  7. *
  8. * 流程步骤:
  9. * 1. 规划师登录系统,生成绑定邀请链接
  10. * 2. 规划师将邀请链接发送给家长
  11. * 3. 家长点击链接,查看邀请详情并确认
  12. * 4. 家长授权绑定,成为规划师的客户
  13. * 5. 规划师可在客户列表中看到新绑定的家庭
  14. *
  15. * 关键里程碑:
  16. * - [Milestone-1] 邀请链接生成成功,获得邀请凭证
  17. * - [Milestone-2] 邀请链接验证成功,展示邀请详情
  18. * - [Milestone-3] 家长确认绑定成功,与规划师建立关系
  19. * - [Milestone-4] 规划师客户列表更新,可以看到新家庭
  20. * - [Milestone-5] 解绑成功,关系终止
  21. * ================================================================
  22. *
  23. * 运行:npx playwright test tests/e2e/planner-bind-invite.spec.js
  24. */
  25. const { test, expect } = require('@playwright/test');
  26. test.describe('【场景流程】规划师绑定邀请全流程', () => {
  27. // ===== 场景1:规划师生成邀请 =====
  28. /**
  29. * 场景:规划师登录后生成一个绑定邀请
  30. * 角色:成长规划师
  31. * 触发条件:规划师点击"邀请家长"按钮
  32. */
  33. test('[场景1] 规划师生成邀请链接 - 期望返回邀请凭证', async ({ page }) => {
  34. // ========== Given:规划师进入邀请页面 ==========
  35. await page.goto('/#/pages/guide/bind/invite');
  36. await page.waitForLoadState('networkidle');
  37. // ========== When:点击生成邀请 ==========
  38. await page.waitForSelector('.invite-btn, .generate-invite-btn', { timeout: 10000 });
  39. await page.click('.generate-invite-btn, .create-invite-btn');
  40. // ========== Then:Milestone-1 - 邀请生成成功 ========
  41. await page.waitForSelector('.invite-token, .invite-link, .invite-success', { timeout: 10000 });
  42. // 验证邀请凭证显示
  43. const inviteToken = page.locator('.invite-token, .invite-code');
  44. expect(await inviteToken.isVisible()).toBeTruthy();
  45. // 复制按钮可用
  46. const copyBtn = page.locator('.copy-btn, .copy-invite-btn');
  47. expect(await copyBtn.isVisible()).toBeTruthy();
  48. });
  49. // ===== 场景2:家长验证邀请 =====
  50. /**
  51. * 场景:家长点击邀请链接,系统验证邀请有效性
  52. * 角色:家长
  53. * 触发条件:家长打开邀请链接
  54. */
  55. test('[场景2] 家长验证邀请链接 - 期望展示邀请详情', async ({ page }) => {
  56. // ========== Given:家长打开邀请链接 ==========
  57. await page.goto('/#/pages/guide/bind/validate?token=invite_test_abc123');
  58. await page.waitForLoadState('networkidle');
  59. // ========== When:页面加载验证 ======
  60. await page.waitForSelector('.invite-detail, .guide-info, .invite-validation', { timeout: 10000 });
  61. // ========== Then:Milestone-2 - 邀请验证成功,展示详情 ========
  62. // 验证规划师名称显示
  63. const guideName = page.locator('.guide-name, .guide-nickname');
  64. expect(await guideName.isVisible()).toBeTruthy();
  65. // 验证服务类型显示
  66. const serviceTypes = page.locator('.service-type, .service-tag');
  67. const serviceCount = await serviceTypes.count();
  68. expect(serviceCount).toBeGreaterThan(0);
  69. // 验证有效期显示
  70. const expireTime = page.locator('.expire-time, .valid-period');
  71. if (await expireTime.isVisible()) {
  72. const expireText = await expireTime.textContent();
  73. expect(expireText).toMatch(/\d{4}|\d{2}:\d{2}/);
  74. }
  75. });
  76. /**
  77. * 场景:家长使用过期或无效的邀请链接
  78. * 角色:家长
  79. * 触发条件:链接已过期或不存在
  80. */
  81. test('[场景2b] 使用无效邀请链接 - 期望返回错误提示', async ({ page }) => {
  82. // ========== Given:打开无效邀请链接 ==========
  83. await page.goto('/#/pages/guide/bind/validate?token=invalid_token');
  84. await page.waitForLoadState('networkidle');
  85. // ========== When:系统验证链接 ==========
  86. await page.waitForSelector('.error-tip, .invalid-invite, .invite-error', { timeout: 10000 });
  87. // ========== Then:返回错误信息 ========
  88. const errorTip = page.locator('.error-tip, .invalid-invite, .invite-error');
  89. expect(await errorTip.isVisible()).toBeTruthy();
  90. const errorText = await errorTip.textContent();
  91. expect(errorText).toMatch(/无效|过期|不存在/);
  92. });
  93. // ===== 场景3:家长确认绑定 =====
  94. /**
  95. * 场景:家长查看邀请详情后,确认绑定该规划师
  96. * 角色:家长
  97. * 触发条件:家长点击"确认绑定"按钮
  98. */
  99. test('[场景3] 家长确认绑定规划师 - 期望绑定成功', async ({ page }) => {
  100. // ========== Given:进入邀请确认页面 ==========
  101. await page.goto('/#/pages/guide/bind/confirm?token=invite_valid_token');
  102. await page.waitForLoadState('networkidle');
  103. // 验证邀请详情已显示
  104. await page.waitForSelector('.guide-info, .invite-detail', { timeout: 10000 });
  105. // ========== Step 1:选择服务类型(可选)==========
  106. const serviceOption = page.locator('.service-option, .service-type-item').first();
  107. if (await serviceOption.isVisible()) {
  108. await serviceOption.click();
  109. }
  110. // ========== When:点击确认绑定 ==========
  111. await page.click('.confirm-bind-btn, .bind-confirm-btn');
  112. // ========== Then:Milestone-3 - 绑定成功 ========
  113. await page.waitForSelector('.success-tip, .bind-success, .binded', { timeout: 10000 });
  114. // 验证绑定成功提示
  115. const successTip = page.locator('.success-tip, .bind-success');
  116. expect(await successTip.isVisible()).toBeTruthy();
  117. });
  118. /**
  119. * 场景:家长尝试重复绑定同一个规划师
  120. * 角色:家长
  121. * 触发条件:已经绑定过该规划师,再次确认绑定
  122. */
  123. test('[场景3b] 重复绑定同一规划师 - 期望返回错误提示', async ({ page }) => {
  124. // ========== Given:已绑定该规划师 ==========
  125. await page.goto('/#/pages/guide/bind/confirm?token=invite_already_bound');
  126. await page.waitForLoadState('networkidle');
  127. // ========== When:点击确认绑定 ==========
  128. await page.click('.confirm-bind-btn');
  129. // ========== Then:返回错误提示 ========
  130. await page.waitForSelector('.error-tip, .already-bound-error', { timeout: 10000 });
  131. const errorTip = page.locator('.error-tip, .already-bound-error');
  132. expect(await errorTip.isVisible()).toBeTruthy();
  133. const errorText = await errorTip.textContent();
  134. expect(errorText).toMatch(/已绑定|重复|已在/);
  135. });
  136. // ===== 场景4:查看绑定规划师列表 =====
  137. /**
  138. * 场景:家长查看已绑定的规划师列表
  139. * 角色:家长
  140. * 触发条件:家长进入"我的规划师"页面
  141. */
  142. test('[场景4] 家长查看绑定规划师列表 - 期望返回已绑定的规划师', async ({ page }) => {
  143. // ========== Given:进入我的规划师页面 ==========
  144. await page.goto('/#/pages/profile/my-guides');
  145. await page.waitForLoadState('networkidle');
  146. // ========== When:查看规划师列表 ==========
  147. await page.waitForSelector('.guide-item, .bound-guide', { timeout: 10000 });
  148. // ========== Then:Milestone-4 - 返回已绑定规划师列表 ========
  149. const guideItems = page.locator('.guide-item, .bound-guide');
  150. const count = await guideItems.count();
  151. expect(count).toBeGreaterThan(0);
  152. // 验证规划师信息显示
  153. const firstGuide = guideItems.first();
  154. const guideName = firstGuide.locator('.guide-name');
  155. expect(await guideName.isVisible()).toBeTruthy();
  156. });
  157. // ===== 场景5:家长解除绑定 =====
  158. /**
  159. * 场景:家长决定解除与某个规划师的绑定关系
  160. * 角色:家长
  161. * 触发条件:家长在规划师详情页点击"解除绑定"
  162. */
  163. test('[场景5] 家长解除绑定规划师 - 期望解除成功并发送通知', async ({ page }) => {
  164. // ========== Given:进入规划师详情页 ==========
  165. await page.goto('/#/pages/guide/detail/1');
  166. await page.waitForLoadState('networkidle');
  167. // ========== Step 1:找到解除绑定按钮 ==========
  168. await page.waitForSelector('.guide-info, .guide-detail', { timeout: 10000 });
  169. const unbindBtn = page.locator('.unbind-btn, .unbind-guide-btn');
  170. // ========== When:点击解除绑定 ==========
  171. if (await unbindBtn.isVisible()) {
  172. await unbindBtn.click();
  173. // 确认解除
  174. await page.waitForSelector('.confirm-dialog, .van-dialog', { timeout: 5000 });
  175. const confirmBtn = page.locator('.confirm-dialog .van-button--primary, .confirm-unbind');
  176. if (await confirmBtn.isVisible()) {
  177. await confirmBtn.click();
  178. }
  179. // ========== Then:Milestone-5 - 解绑成功 ========
  180. await page.waitForSelector('.success-tip, .unbind-success', { timeout: 10000 });
  181. const successTip = page.locator('.success-tip, .unbind-success');
  182. expect(await successTip.isVisible()).toBeTruthy();
  183. }
  184. });
  185. // ===== 场景6:规划师查看已绑定的家庭 =====
  186. /**
  187. * 场景:规划师查看已绑定的家庭列表
  188. * 角色:成长规划师
  189. * 触发条件:规划师进入"我的客户"页面
  190. */
  191. test('[场景6] 规划师查看绑定家庭列表 - 期望返回所有客户家庭', async ({ page }) => {
  192. // ========== Given:规划师进入客户列表 ==========
  193. await page.goto('/#/pages/guide/families');
  194. await page.waitForLoadState('networkidle');
  195. // ========== When:查看客户列表 ==========
  196. await page.waitForSelector('.family-item, .client-family', { timeout: 10000 });
  197. // ========== Then:返回客户家庭列表 ========
  198. const familyItems = page.locator('.family-item, .client-family');
  199. const count = await familyItems.count();
  200. expect(count).toBeGreaterThan(0);
  201. // 验证家庭信息显示
  202. const firstFamily = familyItems.first();
  203. const familyName = firstFamily.locator('.family-name, .family-title');
  204. if (await familyName.isVisible()) {
  205. const nameText = await familyName.textContent();
  206. expect(nameText.length).toBeGreaterThan(0);
  207. }
  208. });
  209. // ===== 场景7:完整绑定流程(端到端)=====
  210. test('[场景7] 完整邀请绑定流程 - 期望全流程成功', async ({ page }) => {
  211. // ========== Step 1:规划师生成邀请 ==========
  212. await page.goto('/#/pages/guide/bind/invite');
  213. await page.waitForLoadState('networkidle');
  214. const generateBtn = page.locator('.generate-invite-btn, .create-invite-btn');
  215. if (await generateBtn.isVisible()) {
  216. await generateBtn.click();
  217. await page.waitForSelector('.invite-token, .invite-link', { timeout: 10000 });
  218. }
  219. // 获取邀请链接
  220. const inviteLink = page.locator('.invite-link');
  221. let inviteUrl = '';
  222. if (await inviteLink.isVisible()) {
  223. inviteUrl = await inviteLink.getAttribute('data-url') || await inviteLink.textContent();
  224. }
  225. // ========== Step 2:家长验证邀请(模拟)==========
  226. await page.goto('/#/pages/guide/bind/validate?token=invite_test_flow');
  227. await page.waitForLoadState('networkidle');
  228. await page.waitForSelector('.guide-info, .invite-detail', { timeout: 10000 });
  229. // ========== Step 3:家长确认绑定 ==========
  230. await page.goto('/#/pages/guide/bind/confirm?token=invite_test_flow');
  231. await page.waitForLoadState('networkidle');
  232. await page.waitForSelector('.guide-info', { timeout: 10000 });
  233. const confirmBindBtn = page.locator('.confirm-bind-btn, .bind-confirm-btn');
  234. if (await confirmBindBtn.isVisible()) {
  235. await confirmBindBtn.click();
  236. await page.waitForSelector('.success-tip, .bind-success', { timeout: 10000 });
  237. }
  238. // ========== Step 4:规划师查看客户列表更新 ==========
  239. await page.goto('/#/pages/guide/families');
  240. await page.waitForLoadState('networkidle');
  241. await page.waitForSelector('.family-item', { timeout: 10000 });
  242. // 验证新绑定的家庭出现在列表中
  243. const familyItems = page.locator('.family-item');
  244. const count = await familyItems.count();
  245. expect(count).toBeGreaterThan(0);
  246. });
  247. });