|
|
@@ -0,0 +1,437 @@
|
|
|
+/**
|
|
|
+ * ================================================================
|
|
|
+ * 场景:健康营养师全流程 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);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+});
|