nutritionist-flow.spec.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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] 营养偏好更新成功
  20. * - [Milestone-3] 饮食记录添加成功
  21. * - [Milestone-4] 食谱创建成功
  22. * - [Milestone-5] 家庭绑定关系建立
  23. * ================================================================
  24. *
  25. * 运行:npx playwright test tests/e2e/nutritionist-flow.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/body/nutrition-profile');
  38. await page.waitForLoadState('networkidle');
  39. // 验证页面加载成功
  40. await expect(page.locator('.nutrition-profile-page')).toBeVisible({ timeout: 10000 });
  41. // ========== When:获取营养档案 ==========
  42. // 等待营养信息加载
  43. await page.waitForSelector('.profile-card, .nutrition-info', { timeout: 10000 });
  44. // ========== Then:Milestone-1 - 返回完整营养档案 ========
  45. const profileCard = page.locator('.profile-card, .nutrition-info');
  46. if (await profileCard.isVisible()) {
  47. // 验证包含关键信息
  48. const profileText = await profileCard.textContent();
  49. expect(profileText.length).toBeGreaterThan(0);
  50. }
  51. // 验证口味偏好存在
  52. const tastePrefs = page.locator('.taste-prefs, .family-taste');
  53. if (await tastePrefs.isVisible()) {
  54. const tasteText = await tastePrefs.textContent();
  55. expect(tasteText).toBeTruthy();
  56. }
  57. });
  58. // ===== 场景2:营养师更新营养偏好 =====
  59. /**
  60. * 场景:营养师根据家庭情况更新营养偏好设置
  61. * 角色:健康营养师
  62. * 触发条件:营养师编辑营养偏好
  63. */
  64. test('[场景2] 营养师更新营养偏好 - 期望保存成功', async ({ page }) => {
  65. // ========== Given:营养师进入营养偏好编辑页 ==========
  66. await page.goto('/#/pages/body/nutrition-profile');
  67. await page.waitForLoadState('networkidle');
  68. // ========== Step 1:点击编辑按钮 ==========
  69. const editBtn = page.locator('.edit-btn, .btn-edit, .edit-profile');
  70. if (await editBtn.isVisible()) {
  71. await editBtn.click();
  72. await page.waitForSelector('.edit-form, .profile-form', { timeout: 5000 });
  73. }
  74. // ========== Step 2:更新口味偏好 ==========
  75. const tasteInput = page.locator('input[name="familyTastePreferences"], .taste-input');
  76. if (await tasteInput.isVisible()) {
  77. await tasteInput.fill('清淡少盐,多蔬菜水果');
  78. }
  79. // ========== Step 3:更新烹饪能力 ==========
  80. const cookingSelect = page.locator('.cooking-ability-select, select[name="cookingAbility"]');
  81. if (await cookingSelect.isVisible()) {
  82. await cookingSelect.selectOption('intermediate');
  83. }
  84. // ========== Step 4:更新月度预算 ==========
  85. const budgetInput = page.locator('input[name="budgetMonthly"], .budget-input');
  86. if (await budgetInput.isVisible()) {
  87. await budgetInput.fill('3000');
  88. }
  89. // ========== When:保存修改 ==========
  90. await page.click('.save-btn, .confirm-btn, .submit-btn');
  91. // ========== Then:Milestone-2 - 保存成功 ========
  92. await page.waitForSelector('.success-tip, .save-success, .toast-success', { timeout: 10000 });
  93. const successTip = page.locator('.success-tip, .save-success, .toast-success');
  94. expect(await successTip.isVisible()).toBeTruthy();
  95. });
  96. // ===== 场景3:营养师添加饮食记录 =====
  97. /**
  98. * 场景:营养师为家庭成员记录一餐的饮食情况
  99. * 角色:健康营养师
  100. * 触发条件:营养师进入饮食记录页面并添加记录
  101. */
  102. test('[场景3] 营养师添加饮食记录 - 期望记录添加成功', async ({ page }) => {
  103. // ========== Given:营养师进入饮食记录页面 ==========
  104. await page.goto('/#/pages/body/meal-record');
  105. await page.waitForLoadState('networkidle');
  106. // 验证页面加载
  107. await expect(page.locator('.meal-record-page')).toBeVisible({ timeout: 10000 });
  108. // ========== Step 1:选择餐次类型 ==========
  109. const breakfastBtn = page.locator('.meal-type-btn:has-text("早餐"), .meal-type:has-text("早餐")');
  110. if (await breakfastBtn.isVisible()) {
  111. await breakfastBtn.click();
  112. }
  113. // ========== Step 2:输入食物名称 ==========
  114. const foodInput = page.locator('input[name="foodName"], .food-name-input');
  115. if (await foodInput.isVisible()) {
  116. await foodInput.fill('全麦面包 + 牛奶 + 鸡蛋');
  117. }
  118. // ========== Step 3:输入食物份量 ==========
  119. const amountInput = page.locator('input[name="foodAmount"], .amount-input');
  120. if (await amountInput.isVisible()) {
  121. await amountInput.fill('100g + 250ml + 1个');
  122. }
  123. // ========== Step 4:输入营养信息 ==========
  124. const caloriesInput = page.locator('input[name="calories"], .calories-input');
  125. if (await caloriesInput.isVisible()) {
  126. await caloriesInput.fill('350');
  127. }
  128. const proteinInput = page.locator('input[name="protein"], .protein-input');
  129. if (await proteinInput.isVisible()) {
  130. await proteinInput.fill('15');
  131. }
  132. // ========== When:提交记录 ==========
  133. await page.click('.submit-btn, .add-btn, .save-btn');
  134. // ========== Then:Milestone-3 - 记录添加成功 ========
  135. await page.waitForSelector('.success-tip, .record-added, .toast-success', { timeout: 10000 });
  136. const successTip = page.locator('.success-tip, .record-added, .toast-success');
  137. expect(await successTip.isVisible()).toBeTruthy();
  138. });
  139. // ===== 场景4:营养师查看饮食记录列表 =====
  140. /**
  141. * 场景:营养师查看家庭成员最近的饮食记录
  142. * 角色:健康营养师
  143. * 触发条件:营养师需要分析家庭饮食结构
  144. */
  145. test('[场景4] 营养师查看饮食记录列表 - 期望返回记录列表', async ({ page }) => {
  146. // ========== Given:营养师进入饮食记录页面 ==========
  147. await page.goto('/#/pages/body/meal-record');
  148. await page.waitForLoadState('networkidle');
  149. // ========== When:查看记录列表 ==========
  150. await page.waitForSelector('.record-list, .meal-list, .diet-records', { timeout: 10000 });
  151. // ========== Then:返回饮食记录列表 ========
  152. const recordItems = page.locator('.record-item, .meal-item, .diet-record');
  153. const count = await recordItems.count();
  154. expect(count).toBeGreaterThanOrEqual(0);
  155. // 验证记录包含日期信息
  156. if (count > 0) {
  157. const firstRecord = recordItems.first();
  158. expect(await firstRecord.isVisible()).toBeTruthy();
  159. }
  160. });
  161. // ===== 场景5:营养师分析营养摄入 =====
  162. /**
  163. * 场景:营养师分析家庭成员的营养摄入情况
  164. * 角色:健康营养师
  165. * 触发条件:需要生成营养分析报告
  166. */
  167. test('[场景5] 营养师分析营养摄入 - 期望返回分析报告', async ({ page }) => {
  168. // ========== Given:营养师进入营养分析页面 ==========
  169. await page.goto('/#/pages/body/nutrition-analysis');
  170. await page.waitForLoadState('networkidle');
  171. // ========== When:生成分析报告 ==========
  172. const analyzeBtn = page.locator('.analyze-btn, .generate-report-btn');
  173. if (await analyzeBtn.isVisible()) {
  174. await analyzeBtn.click();
  175. }
  176. // ========== Then:返回分析结果 ========
  177. await page.waitForSelector('.analysis-result, .nutrition-report, .report-card', { timeout: 15000 });
  178. const reportCard = page.locator('.analysis-result, .nutrition-report, .report-card');
  179. expect(await reportCard.isVisible()).toBeTruthy();
  180. // 验证包含营养素分析
  181. const nutrients = page.locator('.nutrient-item, .macro-item');
  182. if (await nutrients.first().isVisible()) {
  183. const nutrientCount = await nutrients.count();
  184. expect(nutrientCount).toBeGreaterThan(0);
  185. }
  186. });
  187. // ===== 场景6:营养师创建食谱 =====
  188. /**
  189. * 场景:营养师为家庭创建健康食谱
  190. * 角色:健康营养师
  191. * 触发条件:营养师进入食谱管理页面
  192. */
  193. test('[场景6] 营养师创建食谱 - 期望食谱创建成功', async ({ page }) => {
  194. // ========== Given:营养师进入食谱管理页面 ==========
  195. await page.goto('/#/pages/body/recipe-recommend');
  196. await page.waitForLoadState('networkidle');
  197. // ========== Step 1:点击创建食谱 ==========
  198. const createBtn = page.locator('.create-btn, .add-recipe-btn, .create-recipe');
  199. if (await createBtn.isVisible()) {
  200. await createBtn.click();
  201. } else {
  202. // 直接进入创建页
  203. await page.goto('/#/pages/body/recipe-create');
  204. await page.waitForLoadState('networkidle');
  205. }
  206. await page.waitForSelector('.recipe-form, .create-form', { timeout: 5000 });
  207. // ========== Step 2:填写食谱信息 ==========
  208. const nameInput = page.locator('input[name="name"], .recipe-name-input');
  209. if (await nameInput.isVisible()) {
  210. await nameInput.fill('营养早餐套餐_' + Date.now());
  211. }
  212. const ingredientsInput = page.locator('textarea[name="ingredients"], .ingredients-input');
  213. if (await ingredientsInput.isVisible()) {
  214. await ingredientsInput.fill('全麦面包2片、牛奶200ml、鸡蛋1个、水果100g');
  215. }
  216. const methodInput = page.locator('textarea[name="method"], .method-input');
  217. if (await methodInput.isVisible()) {
  218. await methodInput.fill('1. 鸡蛋煮熟切片\n2. 面包烤至微黄\n3. 倒入牛奶\n4. 摆盘水果');
  219. }
  220. // ========== Step 3:填写营养信息 ==========
  221. const caloriesInput = page.locator('input[name="calories"], .calories-input');
  222. if (await caloriesInput.isVisible()) {
  223. await caloriesInput.fill('420');
  224. }
  225. const proteinInput = page.locator('input[name="protein"], .protein-input');
  226. if (await proteinInput.isVisible()) {
  227. await proteinInput.fill('18');
  228. }
  229. // ========== When:提交食谱 ==========
  230. await page.click('.submit-btn, .save-btn, .publish-btn');
  231. // ========== Then:Milestone-4 - 食谱创建成功 ========
  232. await page.waitForSelector('.success-tip, .recipe-created, .toast-success', { timeout: 10000 });
  233. const successTip = page.locator('.success-tip, .recipe-created, .toast-success');
  234. expect(await successTip.isVisible()).toBeTruthy();
  235. });
  236. // ===== 场景7:营养师管理食谱库 =====
  237. /**
  238. * 场景:营养师编辑或删除已有食谱
  239. * 角色:健康营养师
  240. * 触发条件:食谱需要更新或清理
  241. */
  242. test('[场景7] 营养师管理食谱库 - 期望食谱更新/删除成功', async ({ page }) => {
  243. // ========== Given:营养师进入食谱列表页面 ==========
  244. await page.goto('/#/pages/body/recipe-list');
  245. await page.waitForLoadState('networkidle');
  246. // ========== Step 1:找到要编辑的食谱 ==========
  247. await page.waitForSelector('.recipe-item, .recipe-card', { timeout: 10000 });
  248. const recipeItem = page.locator('.recipe-item, .recipe-card').first();
  249. if (await recipeItem.isVisible()) {
  250. // 点击编辑
  251. const editBtn = recipeItem.locator('.edit-btn, .btn-edit');
  252. if (await editBtn.isVisible()) {
  253. await editBtn.click();
  254. } else {
  255. await recipeItem.click();
  256. }
  257. await page.waitForSelector('.edit-form, .recipe-form', { timeout: 5000 });
  258. // ========== Step 2:修改食谱 ==========
  259. const nameInput = page.locator('input[name="name"], .recipe-name-input');
  260. if (await nameInput.isVisible()) {
  261. const newName = '已更新食谱_' + Date.now();
  262. await nameInput.fill(newName);
  263. }
  264. // ========== When:保存修改 ==========
  265. await page.click('.save-btn, .confirm-btn');
  266. await page.waitForSelector('.success-tip, .update-success', { timeout: 10000 });
  267. const successTip = page.locator('.success-tip, .update-success');
  268. expect(await successTip.isVisible()).toBeTruthy();
  269. }
  270. });
  271. // ===== 场景8:营养师查看应季食材 =====
  272. /**
  273. * 场景:营养师查看当前季节的推荐食材
  274. * 角色:健康营养师
  275. * 触发条件:需要制定应季菜单
  276. */
  277. test('[场景8] 营养师查看应季食材 - 期望返回应季食材列表', async ({ page }) => {
  278. // ========== Given:营养师进入应季食材页面 ==========
  279. await page.goto('/#/pages/body/seasonal-foods');
  280. await page.waitForLoadState('networkidle');
  281. // ========== When:查看应季食材 ==========
  282. await page.waitForSelector('.seasonal-list, .food-list, .seasonal-foods', { timeout: 10000 });
  283. // ========== Then:返回应季食材列表 ========
  284. const foodItems = page.locator('.food-item, .seasonal-item');
  285. const count = await foodItems.count();
  286. expect(count).toBeGreaterThanOrEqual(0);
  287. if (count > 0) {
  288. // 验证包含食材名称
  289. const firstItem = foodItems.first();
  290. expect(await firstItem.isVisible()).toBeTruthy();
  291. // 验证包含季节标识
  292. const seasonTag = page.locator('.season-tag, .month-tag');
  293. if (await seasonTag.first().isVisible()) {
  294. const tagText = await seasonTag.first().textContent();
  295. expect(tagText).toBeTruthy();
  296. }
  297. }
  298. });
  299. // ===== 场景9:营养师获取饮食推荐 =====
  300. /**
  301. * 场景:营养师根据家庭偏好获取饮食推荐
  302. * 角色:健康营养师
  303. * 触发条件:需要为家庭推荐合适的三餐
  304. */
  305. test('[场景9] 营养师获取饮食推荐 - 期望返回个性化推荐', async ({ page }) => {
  306. // ========== Given:营养师进入饮食推荐页面 ==========
  307. await page.goto('/#/pages/meal/recommend');
  308. await page.waitForLoadState('networkidle');
  309. // ========== When:获取推荐 ==========
  310. await page.waitForSelector('.recommend-list, .meal-recommend, .recommend-cards', { timeout: 10000 });
  311. // ========== Then:返回推荐列表 ========
  312. const recommendCards = page.locator('.recommend-card, .meal-item, .recommend-item');
  313. const count = await recommendCards.count();
  314. expect(count).toBeGreaterThan(0);
  315. // 验证推荐包含营养信息
  316. const firstCard = recommendCards.first();
  317. expect(await firstCard.isVisible()).toBeTruthy();
  318. const nutritionInfo = page.locator('.nutrition-tag, .calories-tag');
  319. if (await nutritionInfo.first().isVisible()) {
  320. const tagText = await nutritionInfo.first().textContent();
  321. expect(tagText).toBeTruthy();
  322. }
  323. });
  324. // ===== 场景10:营养师绑定家庭 =====
  325. /**
  326. * 场景:营养师接受家庭邀请,建立服务关系
  327. * 角色:健康营养师
  328. * 触发条件:家长向营养师发送邀请
  329. */
  330. test('[场景10] 营养师绑定家庭 - 期望绑定成功', async ({ page }) => {
  331. // ========== Given:营养师收到绑定邀请 ==========
  332. await page.goto('/#/pages/invite/join');
  333. await page.waitForLoadState('networkidle');
  334. // ========== When:接受绑定邀请 ==========
  335. const acceptBtn = page.locator('.accept-btn, .confirm-bind-btn, .accept-invite');
  336. if (await acceptBtn.isVisible()) {
  337. await acceptBtn.click();
  338. }
  339. // 等待确认弹窗
  340. const confirmDialog = page.locator('.van-dialog, .confirm-dialog');
  341. if (await confirmDialog.isVisible()) {
  342. const confirmBtn = page.locator('.van-dialog__confirm, .dialog-confirm-btn');
  343. if (await confirmBtn.isVisible()) {
  344. await confirmBtn.click();
  345. }
  346. }
  347. // ========== Then:Milestone-5 - 绑定成功 ========
  348. await page.waitForSelector('.bind-success, .success-tip, .toast-success', { timeout: 10000 });
  349. // 验证绑定关系建立
  350. const bindIndicator = page.locator('.bind-status, .relation-status');
  351. if (await bindIndicator.isVisible()) {
  352. const statusText = await bindIndicator.textContent();
  353. expect(statusText).toMatch(/已绑定|服务中|active/i);
  354. }
  355. });
  356. });