/** * ================================================================ * 场景:家庭成员管理全流程 E2E 测试 * ================================================================ * 用户故事:作为家庭管理员(家长),我希望管理家庭成员, * 以便正确维护家庭关系结构并授权其他成员使用。 * * 流程步骤: * 1. 管理员进入家庭成员管理页面 * 2. 管理员获取可用的关系类型列表 * 3. 管理员添加新家庭成员,指定关系类型 * 4. 管理员查看当前家庭成员列表 * 5. 管理员编辑成员信息(如修改关系类型) * 6. 管理员切换到成员视角,体验成员端功能 * 7. 管理员移除不需要的成员 * * 关键里程碑: * - [Milestone-1] 成功获取关系类型列表 * - [Milestone-2] 新成员添加成功,获得成员ID * - [Milestone-3] 成员信息更新成功 * - [Milestone-4] 成功切换到成员视角,获得切换凭证 * - [Milestone-5] 成员移除成功 * ================================================================ * * 运行:npx playwright test tests/e2e/family-member-management.spec.js */ const { test, expect } = require('@playwright/test'); test.describe('【场景流程】家庭成员管理全流程', () => { // ===== 场景1:获取关系类型列表 ===== /** * 场景:管理员进入添加成员页面,需要选择关系类型 * 角色:家长(管理员) * 触发条件:打开添加家庭成员页面 */ test('[场景1] 获取关系类型列表 - 期望返回可用的关系类型', async ({ page }) => { // ========== Given:进入添加家庭成员页面 ========== await page.goto('/#/pages/family/add-member'); await page.waitForLoadState('networkidle'); // ========== When:请求关系类型列表 ========== // 关系类型通常在选择器中自动加载 await page.waitForSelector('.relationship-picker, .relation-type-selector', { timeout: 10000 }); // ========== Then:Milestone-1 - 返回完整的关系类型 ======== const relationOptions = page.locator('.relationship-picker .option, .relation-item, .type-option'); const count = await relationOptions.count(); expect(count).toBeGreaterThan(0); // 验证常见的关系类型存在 const pickerOptions = page.locator('.relationship-picker'); if (await pickerOptions.isVisible()) { await pickerOptions.click(); await page.waitForSelector('.option-item', { timeout: 5000 }); // 验证至少包含父母或孩子类型 const optionList = page.locator('.option-item'); const hasParentType = await page.locator('.option-item:has-text("父"), .option-item:has-text("母")').count() > 0; const hasChildType = await page.locator('.option-item:has-text("子"), .option-item:has-text("女")').count() > 0; expect(hasParentType || hasChildType).toBeTruthy(); } }); // ===== 场景2:添加新家庭成员 ===== /** * 场景:家长添加一个新的孩子成员到家庭 * 角色:家长(管理员) * 触发条件:填写完成员信息并提交 */ test('[场景2] 添加新家庭成员 - 期望成员添加成功并返回完整信息', async ({ page }) => { // ========== Given:进入添加成员页面 ========== await page.goto('/#/pages/family/add-member'); await page.waitForLoadState('networkidle'); // ========== Step 1:填写成员信息 ========== // 输入姓名 const nameInput = page.locator('input[placeholder*="姓名"], .name-input, input[name="nickname"]'); if (await nameInput.isVisible()) { await nameInput.fill('王小明'); } // 选择性别 const genderMale = page.locator('.gender-option:has-text("男"), .gender-radio:has-text("男")'); if (await genderMale.isVisible()) { await genderMale.click(); } // 输入生日 const birthdayInput = page.locator('input[placeholder*="生日"], .birthday-input'); if (await birthdayInput.isVisible()) { await birthdayInput.fill('2015-06-15'); } // ========== Step 2:选择关系类型 ========== const relationPicker = page.locator('.relationship-picker, .relation-selector'); if (await relationPicker.isVisible()) { await relationPicker.click(); await page.waitForSelector('.option-item', { timeout: 5000 }); // 选择"儿子"或"女儿" const childOption = page.locator('.option-item:has-text("儿子"), .option-item:has-text("女儿"), .option-item:has-text("孩子")').first(); if (await childOption.isVisible()) { await childOption.click(); } else { // 如果没有精确匹配,点击第一个 await page.locator('.option-item').first().click(); } } // ========== When:提交添加 ========== await page.click('.submit-btn, .add-btn, .confirm-btn'); // ========== Then:Milestone-2 - 成员添加成功 ======== await page.waitForSelector('.success-tip, .add-success, .member-added', { timeout: 10000 }); // 验证添加成功提示 const successTip = page.locator('.success-tip, .add-success, .toast-success'); expect(await successTip.isVisible()).toBeTruthy(); }); /** * 场景:家长添加成员时未填写必填信息(姓名) * 角色:家长 * 触发条件:提交表单时 nickname 为空 */ test('[场景2b] 添加成员时姓名为空 - 期望返回错误提示', async ({ page }) => { // ========== Given:进入添加成员页面 ========== await page.goto('/#/pages/family/add-member'); await page.waitForLoadState('networkidle'); // ========== Step 1:选择关系类型但不填写姓名 ========== const relationPicker = page.locator('.relationship-picker, .relation-selector'); if (await relationPicker.isVisible()) { await relationPicker.click(); await page.waitForSelector('.option-item', { timeout: 5000 }); await page.locator('.option-item').first().click(); } // ========== When:尝试提交 ========== await page.click('.submit-btn, .add-btn, .confirm-btn'); // ========== Then:返回错误信息 ======== // 验证错误提示出现(姓名不能为空) await page.waitForSelector('.error-tip, .van-field__error, .form-error', { timeout: 5000 }); const errorTip = page.locator('.error-tip, .van-field__error, .form-error'); expect(await errorTip.isVisible()).toBeTruthy(); }); // ===== 场景3:查看家庭成员列表 ===== /** * 场景:家长查看当前家庭的所有成员 * 角色:家长 * 触发条件:进入家庭成员管理页面 */ test('[场景3] 查看家庭成员列表 - 期望返回所有成员及关系信息', async ({ page }) => { // ========== Given:进入家庭成员管理页面 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForLoadState('networkidle'); // ========== When:查看成员列表 ========== await page.waitForSelector('.member-item, .family-member', { timeout: 10000 }); // ========== Then:返回完整的成员列表 ======== const memberItems = page.locator('.member-item, .family-member'); const count = await memberItems.count(); expect(count).toBeGreaterThan(0); // 验证每个成员都显示关系类型 const firstMember = memberItems.first(); expect(await firstMember.isVisible()).toBeTruthy(); // 验证关系标签存在 const relationTag = page.locator('.relation-tag, .member-role, .relationship-label'); if (await relationTag.first().isVisible()) { const tagText = await relationTag.first().textContent(); expect(tagText.length).toBeGreaterThan(0); } }); // ===== 场景4:更新成员信息 ===== /** * 场景:家长需要修改成员的关系类型(如孩子长大后变更角色) * 角色:家长 * 触发条件:编辑成员信息并保存 */ test('[场景4] 更新成员关系类型 - 期望更新成功并返回新信息', async ({ page }) => { // ========== Given:进入家庭成员管理页面 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForLoadState('networkidle'); // ========== Step 1:找到要编辑的成员 ========== await page.waitForSelector('.member-item, .family-member', { timeout: 10000 }); const firstMember = page.locator('.member-item, .family-member').first(); // 点击编辑按钮 const editBtn = firstMember.locator('.edit-btn, .btn-edit'); if (await editBtn.isVisible()) { await editBtn.click(); } else { // 直接点击成员项进入编辑页 await firstMember.click(); } // ========== Step 2:修改关系类型 ========== await page.waitForSelector('.edit-form, .member-edit-form', { timeout: 5000 }); const relationPicker = page.locator('.relationship-picker, .relation-selector'); if (await relationPicker.isVisible()) { await relationPicker.click(); await page.waitForSelector('.option-item', { timeout: 5000 }); // 选择一个新的关系类型 await page.locator('.option-item').nth(2).click(); } // ========== When:保存修改 ========== await page.click('.save-btn, .confirm-btn'); // ========== Then:Milestone-3 - 更新成功 ======== await page.waitForSelector('.success-tip, .update-success', { timeout: 10000 }); const successTip = page.locator('.success-tip, .update-success'); expect(await successTip.isVisible()).toBeTruthy(); }); // ===== 场景5:切换到成员视角 ===== /** * 场景:家长需要以孩子视角体验功能,验证孩子端显示是否正常 * 角色:家长(代理操作) * 触发条件:选择成员并点击"切换视角" */ test('[场景5] 切换到成员视角 - 期望返回切换凭证并正确设置上下文', async ({ page }) => { // ========== Given:进入家庭成员管理页面 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForLoadState('networkidle'); // ========== Step 1:找到要切换的成员 ========== await page.waitForSelector('.member-item, .family-member', { timeout: 10000 }); // 找到切换按钮 const switchBtn = page.locator('.switch-btn, .switch-view-btn, .btn-switch'); if (await switchBtn.isVisible()) { await switchBtn.first().click(); } // ========== When:执行切换视角操作 ========== // 如果有成员选择列表,先选择成员 const memberOption = page.locator('.member-option, .switch-member-item').first(); if (await memberOption.isVisible()) { await memberOption.click(); } // 确认切换 const confirmSwitch = page.locator('.confirm-switch-btn, .btn-confirm-switch'); if (await confirmSwitch.isVisible()) { await confirmSwitch.click(); } // ========== Then:Milestone-4 - 切换成功 ======== // 等待页面刷新到孩子视角 await page.waitForTimeout(1000); // 验证切换成功(通常是页面标题或内容变化) const childIndicator = page.locator('.child-view, .child-mode, .switched-indicator'); const hasSwitched = await childIndicator.isVisible() || page.url().includes('child') || page.url().includes('child-index'); expect(hasSwitched).toBeTruthy(); }); /** * 场景:家长尝试切换到自己(不允许自己切换到自己) * 角色:家长 * 触发条件:选择成员ID为自己 */ test('[场景5b] 切换到本人视角 - 期望返回错误提示', async ({ page }) => { // ========== Given:进入切换视角页面 ========== await page.goto('/#/pages/family/switch-view'); await page.waitForLoadState('networkidle'); // ========== When:尝试切换到当前用户自己 ========== // 选择自己的选项(通常被禁用或隐藏) const selfOption = page.locator('.member-option:has-text("我"), .self-option'); if (await selfOption.isVisible()) { // 如果可见,尝试点击(应该被禁用) await selfOption.click(); // 验证按钮被禁用 const switchBtn = page.locator('.switch-btn, .confirm-btn'); const isDisabled = await switchBtn.getAttribute('disabled'); expect(isDisabled).not.toBeNull(); } else { // 如果没有显示自己,说明设计正确(不允许切换到自己) expect(true).toBeTruthy(); } }); // ===== 场景6:移除家庭成员 ===== /** * 场景:家庭成员关系结束,家长移除不再属于家庭的成员 * 角色:家长(管理员) * 触发条件:确认移除操作 */ test('[场景6] 移除家庭成员 - 期望成员从家庭中移除', async ({ page }) => { // ========== Given:进入家庭成员管理页面 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForLoadState('networkidle'); // ========== Step 1:找到要移除的成员 ========== await page.waitForSelector('.member-item, .family-member', { timeout: 10000 }); // 获取移除前的成员数量 const beforeCount = await page.locator('.member-item, .family-member').count(); // 找到最后一个成员(通常是测试数据) const lastMember = page.locator('.member-item, .family-member').last(); const removeBtn = lastMember.locator('.remove-btn, .delete-btn, .btn-remove'); if (await removeBtn.isVisible()) { await removeBtn.click(); // ========== Step 2:确认移除 ========== // 等待确认弹窗 await page.waitForSelector('.confirm-dialog, .van-dialog, .remove-confirm', { timeout: 5000 }); const confirmBtn = page.locator('.confirm-dialog .van-button--primary, .confirm-remove-btn, .btn-confirm'); if (await confirmBtn.isVisible()) { await confirmBtn.click(); } // ========== Then:Milestone-5 - 移除成功 ======== await page.waitForTimeout(1000); // 验证成员数量减少 const afterCount = await page.locator('.member-item, .family-member').count(); expect(afterCount).toBeLessThan(beforeCount); } }); /** * 场景:普通成员(非管理员)尝试踢出成员 * 角色:普通家庭成员(非管理员) * 触发条件:普通成员尝试执行踢人操作 */ test('[场景6b] 普通成员尝试踢人 - 期望权限不足拒绝', async ({ page }) => { // ========== Given:普通成员登录并进入家庭成员页面 ========== // 先登出并用孩子账号登录 await page.goto('/#/pages/login/login'); await page.waitForLoadState('networkidle'); // 模拟孩子登录(实际测试中需要真实的登录流程) // ... // ========== When:尝试踢人操作 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForLoadState('networkidle'); // 检查是否有移除按钮 const removeBtn = page.locator('.remove-btn, .delete-btn, .btn-remove'); // ========== Then:权限不足,拒绝操作 ======== if (await removeBtn.isVisible()) { // 如果显示了按钮,尝试点击应该被拒绝 await removeBtn.click(); // 应该显示权限错误 await page.waitForSelector('.auth-error, .permission-denied, .no-permission', { timeout: 5000 }); const errorTip = page.locator('.auth-error, .permission-denied, .no-permission'); expect(await errorTip.isVisible()).toBeTruthy(); } else { // 如果没有显示按钮,说明权限控制正确 expect(true).toBeTruthy(); } }); // ===== 场景7:完整家庭成员管理流程(端到端)===== test('[场景7] 完整家庭成员管理流程 - 期望全流程成功', async ({ page }) => { // ========== Step 1:进入添加成员页面 ========== await page.goto('/#/pages/family/add-member'); await page.waitForLoadState('networkidle'); // 验证关系类型加载 await page.waitForSelector('.relationship-picker', { timeout: 10000 }); // ========== Step 2:填写并提交新成员 ========== const nameInput = page.locator('input[name="nickname"], .name-input'); if (await nameInput.isVisible()) { await nameInput.fill('测试成员_' + Date.now()); } const genderMale = page.locator('.gender-option:has-text("男")'); if (await genderMale.isVisible()) { await genderMale.click(); } // 选择关系类型 const relationPicker = page.locator('.relationship-picker'); if (await relationPicker.isVisible()) { await relationPicker.click(); await page.waitForSelector('.option-item', { timeout: 5000 }); await page.locator('.option-item').nth(2).click(); } await page.click('.submit-btn'); await page.waitForSelector('.success-tip, .add-success', { timeout: 10000 }); // ========== Step 3:查看成员列表 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForSelector('.member-item', { timeout: 10000 }); const memberCount = await page.locator('.member-item').count(); expect(memberCount).toBeGreaterThan(0); // ========== Step 4:编辑成员 ========== const firstMember = page.locator('.member-item').first(); const editBtn = firstMember.locator('.edit-btn'); if (await editBtn.isVisible()) { await editBtn.click(); await page.waitForSelector('.edit-form', { timeout: 5000 }); await page.click('.save-btn'); await page.waitForSelector('.success-tip', { timeout: 10000 }); } // ========== Step 5:切换视角 ========== const switchBtn = page.locator('.switch-btn').first(); if (await switchBtn.isVisible()) { await switchBtn.click(); await page.waitForTimeout(1000); } // ========== Step 6:返回并移除成员 ========== await page.goto('/#/pages/profile/family-members'); await page.waitForSelector('.member-item', { timeout: 10000 }); const lastMember = page.locator('.member-item').last(); const removeBtn = lastMember.locator('.remove-btn'); if (await removeBtn.isVisible()) { await removeBtn.click(); await page.waitForSelector('.confirm-dialog', { timeout: 5000 }); await page.click('.confirm-dialog .van-button--primary'); await page.waitForTimeout(500); } }); });