/** * ================================================================ * 场景:健康营养师全流程 E2E 测试 * ================================================================ * 用户故事:作为健康营养师,我希望能家庭提供专业的营养指导服务, * 以便帮助家庭成员建立健康的饮食习惯。 * * 流程步骤: * 1. 营养师登录系统,查看营养偏好 * 2. 营养师更新家庭营养档案(口味/预算/烹饪能力) * 3. 营养师添加家庭成员的饮食记录 * 4. 营养师查看饮食记录列表和分析报告 * 5. 营养师创建和管理食谱 * 6. 营养师查看应季食材 * 7. 营养师绑定家庭,开始服务 * * 关键里程碑: * - [Milestone-1] 营养师成功获取家庭营养档案 * - [Milestone-2] 营养偏好更新成功 * - [Milestone-3] 饮食记录添加成功 * - [Milestone-4] 食谱创建成功 * - [Milestone-5] 家庭绑定关系建立 * ================================================================ * * 运行:npx playwright test tests/e2e/nutritionist-flow.spec.js */ const { test, expect } = require('@playwright/test'); test.describe('【场景流程】健康营养师全流程', () => { // ===== 场景1:营养师登录并查看营养偏好 ===== /** * 场景:营养师登录后,查看家庭的营养档案 * 角色:健康营养师 * 触发条件:营养师进入营养偏好页面 */ test('[场景1] 营养师查看家庭营养档案 - 期望返回完整营养信息', async ({ page }) => { // ========== Given:营养师已登录,进入营养偏好页面 ========== await page.goto('/#/pages/body/nutrition-profile'); await page.waitForLoadState('networkidle'); // 验证页面加载成功 await expect(page.locator('.nutrition-profile-page')).toBeVisible({ timeout: 10000 }); // ========== When:获取营养档案 ========== // 等待营养信息加载 await page.waitForSelector('.profile-card, .nutrition-info', { timeout: 10000 }); // ========== Then:Milestone-1 - 返回完整营养档案 ======== const profileCard = page.locator('.profile-card, .nutrition-info'); if (await profileCard.isVisible()) { // 验证包含关键信息 const profileText = await profileCard.textContent(); expect(profileText.length).toBeGreaterThan(0); } // 验证口味偏好存在 const tastePrefs = page.locator('.taste-prefs, .family-taste'); if (await tastePrefs.isVisible()) { const tasteText = await tastePrefs.textContent(); expect(tasteText).toBeTruthy(); } }); // ===== 场景2:营养师更新营养偏好 ===== /** * 场景:营养师根据家庭情况更新营养偏好设置 * 角色:健康营养师 * 触发条件:营养师编辑营养偏好 */ test('[场景2] 营养师更新营养偏好 - 期望保存成功', async ({ page }) => { // ========== Given:营养师进入营养偏好编辑页 ========== await page.goto('/#/pages/body/nutrition-profile'); await page.waitForLoadState('networkidle'); // ========== Step 1:点击编辑按钮 ========== const editBtn = page.locator('.edit-btn, .btn-edit, .edit-profile'); if (await editBtn.isVisible()) { await editBtn.click(); await page.waitForSelector('.edit-form, .profile-form', { timeout: 5000 }); } // ========== Step 2:更新口味偏好 ========== const tasteInput = page.locator('input[name="familyTastePreferences"], .taste-input'); if (await tasteInput.isVisible()) { await tasteInput.fill('清淡少盐,多蔬菜水果'); } // ========== Step 3:更新烹饪能力 ========== const cookingSelect = page.locator('.cooking-ability-select, select[name="cookingAbility"]'); if (await cookingSelect.isVisible()) { await cookingSelect.selectOption('intermediate'); } // ========== Step 4:更新月度预算 ========== const budgetInput = page.locator('input[name="budgetMonthly"], .budget-input'); if (await budgetInput.isVisible()) { await budgetInput.fill('3000'); } // ========== When:保存修改 ========== await page.click('.save-btn, .confirm-btn, .submit-btn'); // ========== Then:Milestone-2 - 保存成功 ======== await page.waitForSelector('.success-tip, .save-success, .toast-success', { timeout: 10000 }); const successTip = page.locator('.success-tip, .save-success, .toast-success'); expect(await successTip.isVisible()).toBeTruthy(); }); // ===== 场景3:营养师添加饮食记录 ===== /** * 场景:营养师为家庭成员记录一餐的饮食情况 * 角色:健康营养师 * 触发条件:营养师进入饮食记录页面并添加记录 */ test('[场景3] 营养师添加饮食记录 - 期望记录添加成功', async ({ page }) => { // ========== Given:营养师进入饮食记录页面 ========== await page.goto('/#/pages/body/meal-record'); await page.waitForLoadState('networkidle'); // 验证页面加载 await expect(page.locator('.meal-record-page')).toBeVisible({ timeout: 10000 }); // ========== Step 1:选择餐次类型 ========== const breakfastBtn = page.locator('.meal-type-btn:has-text("早餐"), .meal-type:has-text("早餐")'); if (await breakfastBtn.isVisible()) { await breakfastBtn.click(); } // ========== Step 2:输入食物名称 ========== const foodInput = page.locator('input[name="foodName"], .food-name-input'); if (await foodInput.isVisible()) { await foodInput.fill('全麦面包 + 牛奶 + 鸡蛋'); } // ========== Step 3:输入食物份量 ========== const amountInput = page.locator('input[name="foodAmount"], .amount-input'); if (await amountInput.isVisible()) { await amountInput.fill('100g + 250ml + 1个'); } // ========== Step 4:输入营养信息 ========== const caloriesInput = page.locator('input[name="calories"], .calories-input'); if (await caloriesInput.isVisible()) { await caloriesInput.fill('350'); } const proteinInput = page.locator('input[name="protein"], .protein-input'); if (await proteinInput.isVisible()) { await proteinInput.fill('15'); } // ========== When:提交记录 ========== await page.click('.submit-btn, .add-btn, .save-btn'); // ========== Then:Milestone-3 - 记录添加成功 ======== await page.waitForSelector('.success-tip, .record-added, .toast-success', { timeout: 10000 }); const successTip = page.locator('.success-tip, .record-added, .toast-success'); expect(await successTip.isVisible()).toBeTruthy(); }); // ===== 场景4:营养师查看饮食记录列表 ===== /** * 场景:营养师查看家庭成员最近的饮食记录 * 角色:健康营养师 * 触发条件:营养师需要分析家庭饮食结构 */ test('[场景4] 营养师查看饮食记录列表 - 期望返回记录列表', async ({ page }) => { // ========== Given:营养师进入饮食记录页面 ========== await page.goto('/#/pages/body/meal-record'); await page.waitForLoadState('networkidle'); // ========== When:查看记录列表 ========== await page.waitForSelector('.record-list, .meal-list, .diet-records', { timeout: 10000 }); // ========== Then:返回饮食记录列表 ======== const recordItems = page.locator('.record-item, .meal-item, .diet-record'); const count = await recordItems.count(); expect(count).toBeGreaterThanOrEqual(0); // 验证记录包含日期信息 if (count > 0) { const firstRecord = recordItems.first(); expect(await firstRecord.isVisible()).toBeTruthy(); } }); // ===== 场景5:营养师分析营养摄入 ===== /** * 场景:营养师分析家庭成员的营养摄入情况 * 角色:健康营养师 * 触发条件:需要生成营养分析报告 */ test('[场景5] 营养师分析营养摄入 - 期望返回分析报告', async ({ page }) => { // ========== Given:营养师进入营养分析页面 ========== await page.goto('/#/pages/body/nutrition-analysis'); await page.waitForLoadState('networkidle'); // ========== When:生成分析报告 ========== const analyzeBtn = page.locator('.analyze-btn, .generate-report-btn'); if (await analyzeBtn.isVisible()) { await analyzeBtn.click(); } // ========== Then:返回分析结果 ======== await page.waitForSelector('.analysis-result, .nutrition-report, .report-card', { timeout: 15000 }); const reportCard = page.locator('.analysis-result, .nutrition-report, .report-card'); expect(await reportCard.isVisible()).toBeTruthy(); // 验证包含营养素分析 const nutrients = page.locator('.nutrient-item, .macro-item'); if (await nutrients.first().isVisible()) { const nutrientCount = await nutrients.count(); expect(nutrientCount).toBeGreaterThan(0); } }); // ===== 场景6:营养师创建食谱 ===== /** * 场景:营养师为家庭创建健康食谱 * 角色:健康营养师 * 触发条件:营养师进入食谱管理页面 */ test('[场景6] 营养师创建食谱 - 期望食谱创建成功', async ({ page }) => { // ========== Given:营养师进入食谱管理页面 ========== await page.goto('/#/pages/body/recipe-recommend'); await page.waitForLoadState('networkidle'); // ========== Step 1:点击创建食谱 ========== const createBtn = page.locator('.create-btn, .add-recipe-btn, .create-recipe'); if (await createBtn.isVisible()) { await createBtn.click(); } else { // 直接进入创建页 await page.goto('/#/pages/body/recipe-create'); await page.waitForLoadState('networkidle'); } await page.waitForSelector('.recipe-form, .create-form', { timeout: 5000 }); // ========== Step 2:填写食谱信息 ========== const nameInput = page.locator('input[name="name"], .recipe-name-input'); if (await nameInput.isVisible()) { await nameInput.fill('营养早餐套餐_' + Date.now()); } const ingredientsInput = page.locator('textarea[name="ingredients"], .ingredients-input'); if (await ingredientsInput.isVisible()) { await ingredientsInput.fill('全麦面包2片、牛奶200ml、鸡蛋1个、水果100g'); } const methodInput = page.locator('textarea[name="method"], .method-input'); if (await methodInput.isVisible()) { await methodInput.fill('1. 鸡蛋煮熟切片\n2. 面包烤至微黄\n3. 倒入牛奶\n4. 摆盘水果'); } // ========== Step 3:填写营养信息 ========== const caloriesInput = page.locator('input[name="calories"], .calories-input'); if (await caloriesInput.isVisible()) { await caloriesInput.fill('420'); } const proteinInput = page.locator('input[name="protein"], .protein-input'); if (await proteinInput.isVisible()) { await proteinInput.fill('18'); } // ========== When:提交食谱 ========== await page.click('.submit-btn, .save-btn, .publish-btn'); // ========== Then:Milestone-4 - 食谱创建成功 ======== await page.waitForSelector('.success-tip, .recipe-created, .toast-success', { timeout: 10000 }); const successTip = page.locator('.success-tip, .recipe-created, .toast-success'); expect(await successTip.isVisible()).toBeTruthy(); }); // ===== 场景7:营养师管理食谱库 ===== /** * 场景:营养师编辑或删除已有食谱 * 角色:健康营养师 * 触发条件:食谱需要更新或清理 */ test('[场景7] 营养师管理食谱库 - 期望食谱更新/删除成功', async ({ page }) => { // ========== Given:营养师进入食谱列表页面 ========== await page.goto('/#/pages/body/recipe-list'); await page.waitForLoadState('networkidle'); // ========== Step 1:找到要编辑的食谱 ========== await page.waitForSelector('.recipe-item, .recipe-card', { timeout: 10000 }); const recipeItem = page.locator('.recipe-item, .recipe-card').first(); if (await recipeItem.isVisible()) { // 点击编辑 const editBtn = recipeItem.locator('.edit-btn, .btn-edit'); if (await editBtn.isVisible()) { await editBtn.click(); } else { await recipeItem.click(); } await page.waitForSelector('.edit-form, .recipe-form', { timeout: 5000 }); // ========== Step 2:修改食谱 ========== const nameInput = page.locator('input[name="name"], .recipe-name-input'); if (await nameInput.isVisible()) { const newName = '已更新食谱_' + Date.now(); await nameInput.fill(newName); } // ========== When:保存修改 ========== await page.click('.save-btn, .confirm-btn'); await page.waitForSelector('.success-tip, .update-success', { timeout: 10000 }); const successTip = page.locator('.success-tip, .update-success'); expect(await successTip.isVisible()).toBeTruthy(); } }); // ===== 场景8:营养师查看应季食材 ===== /** * 场景:营养师查看当前季节的推荐食材 * 角色:健康营养师 * 触发条件:需要制定应季菜单 */ test('[场景8] 营养师查看应季食材 - 期望返回应季食材列表', async ({ page }) => { // ========== Given:营养师进入应季食材页面 ========== await page.goto('/#/pages/body/seasonal-foods'); await page.waitForLoadState('networkidle'); // ========== When:查看应季食材 ========== await page.waitForSelector('.seasonal-list, .food-list, .seasonal-foods', { timeout: 10000 }); // ========== Then:返回应季食材列表 ======== const foodItems = page.locator('.food-item, .seasonal-item'); const count = await foodItems.count(); expect(count).toBeGreaterThanOrEqual(0); if (count > 0) { // 验证包含食材名称 const firstItem = foodItems.first(); expect(await firstItem.isVisible()).toBeTruthy(); // 验证包含季节标识 const seasonTag = page.locator('.season-tag, .month-tag'); if (await seasonTag.first().isVisible()) { const tagText = await seasonTag.first().textContent(); expect(tagText).toBeTruthy(); } } }); // ===== 场景9:营养师获取饮食推荐 ===== /** * 场景:营养师根据家庭偏好获取饮食推荐 * 角色:健康营养师 * 触发条件:需要为家庭推荐合适的三餐 */ test('[场景9] 营养师获取饮食推荐 - 期望返回个性化推荐', async ({ page }) => { // ========== Given:营养师进入饮食推荐页面 ========== await page.goto('/#/pages/meal/recommend'); await page.waitForLoadState('networkidle'); // ========== When:获取推荐 ========== await page.waitForSelector('.recommend-list, .meal-recommend, .recommend-cards', { timeout: 10000 }); // ========== Then:返回推荐列表 ======== const recommendCards = page.locator('.recommend-card, .meal-item, .recommend-item'); const count = await recommendCards.count(); expect(count).toBeGreaterThan(0); // 验证推荐包含营养信息 const firstCard = recommendCards.first(); expect(await firstCard.isVisible()).toBeTruthy(); const nutritionInfo = page.locator('.nutrition-tag, .calories-tag'); if (await nutritionInfo.first().isVisible()) { const tagText = await nutritionInfo.first().textContent(); expect(tagText).toBeTruthy(); } }); // ===== 场景10:营养师绑定家庭 ===== /** * 场景:营养师接受家庭邀请,建立服务关系 * 角色:健康营养师 * 触发条件:家长向营养师发送邀请 */ test('[场景10] 营养师绑定家庭 - 期望绑定成功', async ({ page }) => { // ========== Given:营养师收到绑定邀请 ========== await page.goto('/#/pages/invite/join'); await page.waitForLoadState('networkidle'); // ========== When:接受绑定邀请 ========== const acceptBtn = page.locator('.accept-btn, .confirm-bind-btn, .accept-invite'); if (await acceptBtn.isVisible()) { await acceptBtn.click(); } // 等待确认弹窗 const confirmDialog = page.locator('.van-dialog, .confirm-dialog'); if (await confirmDialog.isVisible()) { const confirmBtn = page.locator('.van-dialog__confirm, .dialog-confirm-btn'); if (await confirmBtn.isVisible()) { await confirmBtn.click(); } } // ========== Then:Milestone-5 - 绑定成功 ======== await page.waitForSelector('.bind-success, .success-tip, .toast-success', { timeout: 10000 }); // 验证绑定关系建立 const bindIndicator = page.locator('.bind-status, .relation-status'); if (await bindIndicator.isVisible()) { const statusText = await bindIndicator.textContent(); expect(statusText).toMatch(/已绑定|服务中|active/i); } }); });