| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- /**
- * ================================================================
- * 场景:家庭成员管理全流程 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);
- }
- });
- });
|