family-member-management.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /**
  2. * ================================================================
  3. * 场景:家庭成员管理全流程 E2E 测试
  4. * ================================================================
  5. * 用户故事:作为家庭管理员(家长),我希望管理家庭成员,
  6. * 以便正确维护家庭关系结构并授权其他成员使用。
  7. *
  8. * 流程步骤:
  9. * 1. 管理员进入家庭成员管理页面
  10. * 2. 管理员获取可用的关系类型列表
  11. * 3. 管理员添加新家庭成员,指定关系类型
  12. * 4. 管理员查看当前家庭成员列表
  13. * 5. 管理员编辑成员信息(如修改关系类型)
  14. * 6. 管理员切换到成员视角,体验成员端功能
  15. * 7. 管理员移除不需要的成员
  16. *
  17. * 关键里程碑:
  18. * - [Milestone-1] 成功获取关系类型列表
  19. * - [Milestone-2] 新成员添加成功,获得成员ID
  20. * - [Milestone-3] 成员信息更新成功
  21. * - [Milestone-4] 成功切换到成员视角,获得切换凭证
  22. * - [Milestone-5] 成员移除成功
  23. * ================================================================
  24. *
  25. * 运行:npx playwright test tests/e2e/family-member-management.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/family/add-member');
  38. await page.waitForLoadState('networkidle');
  39. // ========== When:请求关系类型列表 ==========
  40. // 关系类型通常在选择器中自动加载
  41. await page.waitForSelector('.relationship-picker, .relation-type-selector', { timeout: 10000 });
  42. // ========== Then:Milestone-1 - 返回完整的关系类型 ========
  43. const relationOptions = page.locator('.relationship-picker .option, .relation-item, .type-option');
  44. const count = await relationOptions.count();
  45. expect(count).toBeGreaterThan(0);
  46. // 验证常见的关系类型存在
  47. const pickerOptions = page.locator('.relationship-picker');
  48. if (await pickerOptions.isVisible()) {
  49. await pickerOptions.click();
  50. await page.waitForSelector('.option-item', { timeout: 5000 });
  51. // 验证至少包含父母或孩子类型
  52. const optionList = page.locator('.option-item');
  53. const hasParentType = await page.locator('.option-item:has-text("父"), .option-item:has-text("母")').count() > 0;
  54. const hasChildType = await page.locator('.option-item:has-text("子"), .option-item:has-text("女")').count() > 0;
  55. expect(hasParentType || hasChildType).toBeTruthy();
  56. }
  57. });
  58. // ===== 场景2:添加新家庭成员 =====
  59. /**
  60. * 场景:家长添加一个新的孩子成员到家庭
  61. * 角色:家长(管理员)
  62. * 触发条件:填写完成员信息并提交
  63. */
  64. test('[场景2] 添加新家庭成员 - 期望成员添加成功并返回完整信息', async ({ page }) => {
  65. // ========== Given:进入添加成员页面 ==========
  66. await page.goto('/#/pages/family/add-member');
  67. await page.waitForLoadState('networkidle');
  68. // ========== Step 1:填写成员信息 ==========
  69. // 输入姓名
  70. const nameInput = page.locator('input[placeholder*="姓名"], .name-input, input[name="nickname"]');
  71. if (await nameInput.isVisible()) {
  72. await nameInput.fill('王小明');
  73. }
  74. // 选择性别
  75. const genderMale = page.locator('.gender-option:has-text("男"), .gender-radio:has-text("男")');
  76. if (await genderMale.isVisible()) {
  77. await genderMale.click();
  78. }
  79. // 输入生日
  80. const birthdayInput = page.locator('input[placeholder*="生日"], .birthday-input');
  81. if (await birthdayInput.isVisible()) {
  82. await birthdayInput.fill('2015-06-15');
  83. }
  84. // ========== Step 2:选择关系类型 ==========
  85. const relationPicker = page.locator('.relationship-picker, .relation-selector');
  86. if (await relationPicker.isVisible()) {
  87. await relationPicker.click();
  88. await page.waitForSelector('.option-item', { timeout: 5000 });
  89. // 选择"儿子"或"女儿"
  90. const childOption = page.locator('.option-item:has-text("儿子"), .option-item:has-text("女儿"), .option-item:has-text("孩子")').first();
  91. if (await childOption.isVisible()) {
  92. await childOption.click();
  93. } else {
  94. // 如果没有精确匹配,点击第一个
  95. await page.locator('.option-item').first().click();
  96. }
  97. }
  98. // ========== When:提交添加 ==========
  99. await page.click('.submit-btn, .add-btn, .confirm-btn');
  100. // ========== Then:Milestone-2 - 成员添加成功 ========
  101. await page.waitForSelector('.success-tip, .add-success, .member-added', { timeout: 10000 });
  102. // 验证添加成功提示
  103. const successTip = page.locator('.success-tip, .add-success, .toast-success');
  104. expect(await successTip.isVisible()).toBeTruthy();
  105. });
  106. /**
  107. * 场景:家长添加成员时未填写必填信息(姓名)
  108. * 角色:家长
  109. * 触发条件:提交表单时 nickname 为空
  110. */
  111. test('[场景2b] 添加成员时姓名为空 - 期望返回错误提示', async ({ page }) => {
  112. // ========== Given:进入添加成员页面 ==========
  113. await page.goto('/#/pages/family/add-member');
  114. await page.waitForLoadState('networkidle');
  115. // ========== Step 1:选择关系类型但不填写姓名 ==========
  116. const relationPicker = page.locator('.relationship-picker, .relation-selector');
  117. if (await relationPicker.isVisible()) {
  118. await relationPicker.click();
  119. await page.waitForSelector('.option-item', { timeout: 5000 });
  120. await page.locator('.option-item').first().click();
  121. }
  122. // ========== When:尝试提交 ==========
  123. await page.click('.submit-btn, .add-btn, .confirm-btn');
  124. // ========== Then:返回错误信息 ========
  125. // 验证错误提示出现(姓名不能为空)
  126. await page.waitForSelector('.error-tip, .van-field__error, .form-error', { timeout: 5000 });
  127. const errorTip = page.locator('.error-tip, .van-field__error, .form-error');
  128. expect(await errorTip.isVisible()).toBeTruthy();
  129. });
  130. // ===== 场景3:查看家庭成员列表 =====
  131. /**
  132. * 场景:家长查看当前家庭的所有成员
  133. * 角色:家长
  134. * 触发条件:进入家庭成员管理页面
  135. */
  136. test('[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息', async ({ page }) => {
  137. // ========== Given:进入家庭成员管理页面 ==========
  138. await page.goto('/#/pages/profile/family-members');
  139. await page.waitForLoadState('networkidle');
  140. // ========== When:查看成员列表 ==========
  141. await page.waitForSelector('.member-item, .family-member', { timeout: 10000 });
  142. // ========== Then:返回完整的成员列表 ========
  143. const memberItems = page.locator('.member-item, .family-member');
  144. const count = await memberItems.count();
  145. expect(count).toBeGreaterThan(0);
  146. // 验证每个成员都显示关系类型
  147. const firstMember = memberItems.first();
  148. expect(await firstMember.isVisible()).toBeTruthy();
  149. // 验证关系标签存在
  150. const relationTag = page.locator('.relation-tag, .member-role, .relationship-label');
  151. if (await relationTag.first().isVisible()) {
  152. const tagText = await relationTag.first().textContent();
  153. expect(tagText.length).toBeGreaterThan(0);
  154. }
  155. });
  156. // ===== 场景4:更新成员信息 =====
  157. /**
  158. * 场景:家长需要修改成员的关系类型(如孩子长大后变更角色)
  159. * 角色:家长
  160. * 触发条件:编辑成员信息并保存
  161. */
  162. test('[场景4] 更新成员关系类型 - 期望更新成功并返回新信息', async ({ page }) => {
  163. // ========== Given:进入家庭成员管理页面 ==========
  164. await page.goto('/#/pages/profile/family-members');
  165. await page.waitForLoadState('networkidle');
  166. // ========== Step 1:找到要编辑的成员 ==========
  167. await page.waitForSelector('.member-item, .family-member', { timeout: 10000 });
  168. const firstMember = page.locator('.member-item, .family-member').first();
  169. // 点击编辑按钮
  170. const editBtn = firstMember.locator('.edit-btn, .btn-edit');
  171. if (await editBtn.isVisible()) {
  172. await editBtn.click();
  173. } else {
  174. // 直接点击成员项进入编辑页
  175. await firstMember.click();
  176. }
  177. // ========== Step 2:修改关系类型 ==========
  178. await page.waitForSelector('.edit-form, .member-edit-form', { timeout: 5000 });
  179. const relationPicker = page.locator('.relationship-picker, .relation-selector');
  180. if (await relationPicker.isVisible()) {
  181. await relationPicker.click();
  182. await page.waitForSelector('.option-item', { timeout: 5000 });
  183. // 选择一个新的关系类型
  184. await page.locator('.option-item').nth(2).click();
  185. }
  186. // ========== When:保存修改 ==========
  187. await page.click('.save-btn, .confirm-btn');
  188. // ========== Then:Milestone-3 - 更新成功 ========
  189. await page.waitForSelector('.success-tip, .update-success', { timeout: 10000 });
  190. const successTip = page.locator('.success-tip, .update-success');
  191. expect(await successTip.isVisible()).toBeTruthy();
  192. });
  193. // ===== 场景5:切换到成员视角 =====
  194. /**
  195. * 场景:家长需要以孩子视角体验功能,验证孩子端显示是否正常
  196. * 角色:家长(代理操作)
  197. * 触发条件:选择成员并点击"切换视角"
  198. */
  199. test('[场景5] 切换到成员视角 - 期望返回切换凭证并正确设置上下文', async ({ page }) => {
  200. // ========== Given:进入家庭成员管理页面 ==========
  201. await page.goto('/#/pages/profile/family-members');
  202. await page.waitForLoadState('networkidle');
  203. // ========== Step 1:找到要切换的成员 ==========
  204. await page.waitForSelector('.member-item, .family-member', { timeout: 10000 });
  205. // 找到切换按钮
  206. const switchBtn = page.locator('.switch-btn, .switch-view-btn, .btn-switch');
  207. if (await switchBtn.isVisible()) {
  208. await switchBtn.first().click();
  209. }
  210. // ========== When:执行切换视角操作 ==========
  211. // 如果有成员选择列表,先选择成员
  212. const memberOption = page.locator('.member-option, .switch-member-item').first();
  213. if (await memberOption.isVisible()) {
  214. await memberOption.click();
  215. }
  216. // 确认切换
  217. const confirmSwitch = page.locator('.confirm-switch-btn, .btn-confirm-switch');
  218. if (await confirmSwitch.isVisible()) {
  219. await confirmSwitch.click();
  220. }
  221. // ========== Then:Milestone-4 - 切换成功 ========
  222. // 等待页面刷新到孩子视角
  223. await page.waitForTimeout(1000);
  224. // 验证切换成功(通常是页面标题或内容变化)
  225. const childIndicator = page.locator('.child-view, .child-mode, .switched-indicator');
  226. const hasSwitched = await childIndicator.isVisible() ||
  227. page.url().includes('child') ||
  228. page.url().includes('child-index');
  229. expect(hasSwitched).toBeTruthy();
  230. });
  231. /**
  232. * 场景:家长尝试切换到自己(不允许自己切换到自己)
  233. * 角色:家长
  234. * 触发条件:选择成员ID为自己
  235. */
  236. test('[场景5b] 切换到本人视角 - 期望返回错误提示', async ({ page }) => {
  237. // ========== Given:进入切换视角页面 ==========
  238. await page.goto('/#/pages/family/switch-view');
  239. await page.waitForLoadState('networkidle');
  240. // ========== When:尝试切换到当前用户自己 ==========
  241. // 选择自己的选项(通常被禁用或隐藏)
  242. const selfOption = page.locator('.member-option:has-text("我"), .self-option');
  243. if (await selfOption.isVisible()) {
  244. // 如果可见,尝试点击(应该被禁用)
  245. await selfOption.click();
  246. // 验证按钮被禁用
  247. const switchBtn = page.locator('.switch-btn, .confirm-btn');
  248. const isDisabled = await switchBtn.getAttribute('disabled');
  249. expect(isDisabled).not.toBeNull();
  250. } else {
  251. // 如果没有显示自己,说明设计正确(不允许切换到自己)
  252. expect(true).toBeTruthy();
  253. }
  254. });
  255. // ===== 场景6:移除家庭成员 =====
  256. /**
  257. * 场景:家庭成员关系结束,家长移除不再属于家庭的成员
  258. * 角色:家长(管理员)
  259. * 触发条件:确认移除操作
  260. */
  261. test('[场景6] 移除家庭成员 - 期望成员从家庭中移除', async ({ page }) => {
  262. // ========== Given:进入家庭成员管理页面 ==========
  263. await page.goto('/#/pages/profile/family-members');
  264. await page.waitForLoadState('networkidle');
  265. // ========== Step 1:找到要移除的成员 ==========
  266. await page.waitForSelector('.member-item, .family-member', { timeout: 10000 });
  267. // 获取移除前的成员数量
  268. const beforeCount = await page.locator('.member-item, .family-member').count();
  269. // 找到最后一个成员(通常是测试数据)
  270. const lastMember = page.locator('.member-item, .family-member').last();
  271. const removeBtn = lastMember.locator('.remove-btn, .delete-btn, .btn-remove');
  272. if (await removeBtn.isVisible()) {
  273. await removeBtn.click();
  274. // ========== Step 2:确认移除 ==========
  275. // 等待确认弹窗
  276. await page.waitForSelector('.confirm-dialog, .van-dialog, .remove-confirm', { timeout: 5000 });
  277. const confirmBtn = page.locator('.confirm-dialog .van-button--primary, .confirm-remove-btn, .btn-confirm');
  278. if (await confirmBtn.isVisible()) {
  279. await confirmBtn.click();
  280. }
  281. // ========== Then:Milestone-5 - 移除成功 ========
  282. await page.waitForTimeout(1000);
  283. // 验证成员数量减少
  284. const afterCount = await page.locator('.member-item, .family-member').count();
  285. expect(afterCount).toBeLessThan(beforeCount);
  286. }
  287. });
  288. /**
  289. * 场景:普通成员(非管理员)尝试踢出成员
  290. * 角色:普通家庭成员(非管理员)
  291. * 触发条件:普通成员尝试执行踢人操作
  292. */
  293. test('[场景6b] 普通成员尝试踢人 - 期望权限不足拒绝', async ({ page }) => {
  294. // ========== Given:普通成员登录并进入家庭成员页面 ==========
  295. // 先登出并用孩子账号登录
  296. await page.goto('/#/pages/login/login');
  297. await page.waitForLoadState('networkidle');
  298. // 模拟孩子登录(实际测试中需要真实的登录流程)
  299. // ...
  300. // ========== When:尝试踢人操作 ==========
  301. await page.goto('/#/pages/profile/family-members');
  302. await page.waitForLoadState('networkidle');
  303. // 检查是否有移除按钮
  304. const removeBtn = page.locator('.remove-btn, .delete-btn, .btn-remove');
  305. // ========== Then:权限不足,拒绝操作 ========
  306. if (await removeBtn.isVisible()) {
  307. // 如果显示了按钮,尝试点击应该被拒绝
  308. await removeBtn.click();
  309. // 应该显示权限错误
  310. await page.waitForSelector('.auth-error, .permission-denied, .no-permission', { timeout: 5000 });
  311. const errorTip = page.locator('.auth-error, .permission-denied, .no-permission');
  312. expect(await errorTip.isVisible()).toBeTruthy();
  313. } else {
  314. // 如果没有显示按钮,说明权限控制正确
  315. expect(true).toBeTruthy();
  316. }
  317. });
  318. // ===== 场景7:完整家庭成员管理流程(端到端)=====
  319. test('[场景7] 完整家庭成员管理流程 - 期望全流程成功', async ({ page }) => {
  320. // ========== Step 1:进入添加成员页面 ==========
  321. await page.goto('/#/pages/family/add-member');
  322. await page.waitForLoadState('networkidle');
  323. // 验证关系类型加载
  324. await page.waitForSelector('.relationship-picker', { timeout: 10000 });
  325. // ========== Step 2:填写并提交新成员 ==========
  326. const nameInput = page.locator('input[name="nickname"], .name-input');
  327. if (await nameInput.isVisible()) {
  328. await nameInput.fill('测试成员_' + Date.now());
  329. }
  330. const genderMale = page.locator('.gender-option:has-text("男")');
  331. if (await genderMale.isVisible()) {
  332. await genderMale.click();
  333. }
  334. // 选择关系类型
  335. const relationPicker = page.locator('.relationship-picker');
  336. if (await relationPicker.isVisible()) {
  337. await relationPicker.click();
  338. await page.waitForSelector('.option-item', { timeout: 5000 });
  339. await page.locator('.option-item').nth(2).click();
  340. }
  341. await page.click('.submit-btn');
  342. await page.waitForSelector('.success-tip, .add-success', { timeout: 10000 });
  343. // ========== Step 3:查看成员列表 ==========
  344. await page.goto('/#/pages/profile/family-members');
  345. await page.waitForSelector('.member-item', { timeout: 10000 });
  346. const memberCount = await page.locator('.member-item').count();
  347. expect(memberCount).toBeGreaterThan(0);
  348. // ========== Step 4:编辑成员 ==========
  349. const firstMember = page.locator('.member-item').first();
  350. const editBtn = firstMember.locator('.edit-btn');
  351. if (await editBtn.isVisible()) {
  352. await editBtn.click();
  353. await page.waitForSelector('.edit-form', { timeout: 5000 });
  354. await page.click('.save-btn');
  355. await page.waitForSelector('.success-tip', { timeout: 10000 });
  356. }
  357. // ========== Step 5:切换视角 ==========
  358. const switchBtn = page.locator('.switch-btn').first();
  359. if (await switchBtn.isVisible()) {
  360. await switchBtn.click();
  361. await page.waitForTimeout(1000);
  362. }
  363. // ========== Step 6:返回并移除成员 ==========
  364. await page.goto('/#/pages/profile/family-members');
  365. await page.waitForSelector('.member-item', { timeout: 10000 });
  366. const lastMember = page.locator('.member-item').last();
  367. const removeBtn = lastMember.locator('.remove-btn');
  368. if (await removeBtn.isVisible()) {
  369. await removeBtn.click();
  370. await page.waitForSelector('.confirm-dialog', { timeout: 5000 });
  371. await page.click('.confirm-dialog .van-button--primary');
  372. await page.waitForTimeout(500);
  373. }
  374. });
  375. });