diff --git a/tests/e2e/oikos-kitchen-assist-flow.spec.mjs b/tests/e2e/oikos-kitchen-assist-flow.spec.mjs index c7f905c..0df1962 100644 --- a/tests/e2e/oikos-kitchen-assist-flow.spec.mjs +++ b/tests/e2e/oikos-kitchen-assist-flow.spec.mjs @@ -55,9 +55,10 @@ async function openMealPlanStudioFromAssist(page, diagnostics, prompt = 'Lav en await action.click(); await page.waitForURL(/\/meals(?:$|[?#])/, { timeout: 15_000 }); const studioHost = page.locator('[data-oikos-meal-plan-studio]'); - await expect(studioHost.locator('[data-assist-studio-confirm]')).toBeVisible({ timeout: 15_000 }); - await expect(studioHost.locator('[data-assist-studio-title]')).toHaveCount(7, { timeout: 15_000 }); - await expect(studioHost).toContainText(/Madlavere & familiepræferencer|Børnenes madplan|Måltidshistorik/i); + await expect(studioHost.locator('[data-assist-wizard="day"]'), 'Studio should open into the guided day-by-day wizard, not the old all-days form').toBeVisible({ timeout: 15_000 }); + await expect(studioHost.locator('.assist-wizard-steps [data-assist-wizard-step]'), 'wizard should expose the five-step planning contract').toHaveCount(5, { timeout: 15_000 }); + await expect(studioHost.locator('[data-assist-wizard-day]'), 'wizard should summarize every generated day').toHaveCount(7, { timeout: 15_000 }); + await expect(studioHost).toContainText(/Dagstype|Én beslutning ad gangen|Avanceret/i); return studioHost; } @@ -82,6 +83,50 @@ function assertStructuredStudioPlan(plan) { expect(plan.slots.some((slot) => slot.context?.modulators?.easyDay || slot.context?.modulators?.guests || slot.context?.modulators?.noKids || typeof slot.context?.modulators?.cookAge === 'number'), 'generated week should carry actionable day/person modulators').toBeTruthy(); } +async function chooseWizardIntent(studioHost, dayIndex, intent = 'quick') { + await expect(studioHost.locator('[data-assist-wizard="day"]')).toBeVisible({ timeout: 15_000 }); + await studioHost.locator(`[data-assist-wizard-intent="${dayIndex}:${intent}"]`).click(); + await expect(studioHost.locator('[data-assist-wizard="followup"]')).toBeVisible({ timeout: 15_000 }); +} + +async function chooseWizardFollowupAndMeal(studioHost, dayIndex, { followup = 'time_20', refresh = false, manualTitle = '', chooseSuggestion = true } = {}) { + const followupOption = studioHost.locator(`[data-assist-wizard-followup="${dayIndex}:${followup}"]`); + if (await followupOption.count()) await followupOption.click(); + if (refresh) { + await studioHost.locator(`[data-assist-wizard-refresh="${dayIndex}"]`).click(); + } else { + await studioHost.locator('.assist-wizard-steps [data-assist-wizard-step="meal"]').click(); + } + await expect(studioHost.locator('[data-assist-wizard="meal"]')).toBeVisible({ timeout: 30_000 }); + await expect(studioHost.locator(`[data-assist-suggestion-card^="${dayIndex}:"]`), 'meal step should expose suggestion cards before save').toHaveCount(3, { timeout: 15_000 }); + if (manualTitle) { + const manualForm = studioHost.locator(`[data-assist-wizard-manual="${dayIndex}"]`); + await manualForm.locator('input[name="title"]').fill(manualTitle); + await manualForm.locator('button[type="submit"]').click(); + } else if (chooseSuggestion) { + await studioHost.locator(`[data-assist-suggestion-card="${dayIndex}:0"]`).click(); + } +} + +async function completeWizardDay(studioHost, dayIndex) { + await studioHost.locator(`[data-assist-wizard-complete-day="${dayIndex}"]`).click(); +} + +async function completeWizardToReview(studioHost, { firstManualTitle = '', refreshFirstDay = false } = {}) { + for (let dayIndex = 0; dayIndex < 7; dayIndex += 1) { + await chooseWizardIntent(studioHost, dayIndex, dayIndex === 1 ? 'healthy' : 'quick'); + await chooseWizardFollowupAndMeal(studioHost, dayIndex, { + followup: dayIndex === 1 ? 'protein' : 'time_20', + refresh: refreshFirstDay && dayIndex === 0, + manualTitle: dayIndex === 0 ? firstManualTitle : '', + chooseSuggestion: dayIndex !== 0 || !firstManualTitle, + }); + await completeWizardDay(studioHost, dayIndex); + } + await expect(studioHost.locator('[data-assist-wizard="review"]')).toBeVisible({ timeout: 15_000 }); + await expect(studioHost.locator('[data-assist-studio-confirm]'), 'save action should appear only at the review step').toBeVisible({ timeout: 15_000 }); +} + async function deleteMeals(page, mealIds) { if (!mealIds.length) return []; return page.evaluate(async (ids) => { @@ -202,9 +247,16 @@ test.describe('Oikos Kitchen + Assist meal-planning flow', () => { const studioHost = page.locator('[data-oikos-meal-plan-studio]'); await expect(studioHost).toContainText(/Meal Plan Studio/i); - await openMealPlanStudioFromAssist(page, diagnostics); + const routedStudioHost = await openMealPlanStudioFromAssist(page, diagnostics); + await chooseWizardIntent(routedStudioHost, 0, 'quick'); + await chooseWizardFollowupAndMeal(routedStudioHost, 0, { followup: 'time_20', refresh: true }); + await completeWizardDay(routedStudioHost, 0); + await expect(routedStudioHost.locator('[data-assist-wizard="day"]')).toBeVisible({ timeout: 15_000 }); - assertStructuredStudioPlan(await readStudioPlan(page)); + const routedPlan = await readStudioPlan(page); + assertStructuredStudioPlan(routedPlan); + expect(routedPlan.daySetup?.[0]?.intent, 'wizard day intent should persist into the draft plan').toBe('quick'); + expect(routedPlan.daySetup?.[0]?.followups || [], 'wizard follow-up choices should persist after suggestion refresh').toContain('time_20'); expect(diagnostics.pageErrors, 'No browser page errors during Kitchen/Assist flow').toEqual([]); await attachDiagnostics(testInfo, diagnostics); @@ -222,26 +274,7 @@ test.describe('Oikos Kitchen + Assist meal-planning flow', () => { await page.goto('/meals', { waitUntil: 'domcontentloaded' }); await dismissOnboardingIfPresent(page); const studioHost = await openMealPlanStudioFromAssist(page, diagnostics, 'Lav en testbar madplan for næste uge. Brug vores opskrifter og åbn den i Meal Plan Studio.'); - - const firstSwap = studioHost.locator('[data-assist-studio-swap]').first(); - await expect(firstSwap, 'generated Studio plan should expose at least one swap alternative').toBeVisible(); - const swapResponsePromise = page.waitForResponse((res) => res.url().includes('/ai/api/meal-plan/feedback') && res.request().method() === 'POST'); - await firstSwap.click(); - const swapResponse = await swapResponsePromise; - expect(swapResponse.ok(), 'swap should be recorded as meal-plan feedback').toBeTruthy(); - const swapPayload = await swapResponse.json(); - expect(swapPayload.source, 'swap feedback should flow through the native meal-planning API').toBe('oikos-native-api'); - - const firstTitle = studioHost.locator('[data-assist-studio-title="0"]'); - const originalTitle = await firstTitle.inputValue(); - const editedTitle = `[E2E cleanup] ${originalTitle}`.slice(0, 120); - await firstTitle.fill(editedTitle); - const editResponsePromise = page.waitForResponse((res) => res.url().includes('/ai/api/meal-plan/feedback') && res.request().method() === 'POST'); - await firstTitle.dispatchEvent('change'); - const editResponse = await editResponsePromise; - expect(editResponse.ok(), 'title edit should be recorded as meal-plan feedback').toBeTruthy(); - const editPayload = await editResponse.json(); - expect(editPayload.source, 'edit feedback should flow through the native meal-planning API').toBe('oikos-native-api'); + await completeWizardToReview(studioHost, { firstManualTitle: '[E2E cleanup] Guided wizard dinner', refreshFirstDay: true }); assertStructuredStudioPlan(await readStudioPlan(page)); @@ -259,7 +292,7 @@ test.describe('Oikos Kitchen + Assist meal-planning flow', () => { const mealsInOikos = await listMealsById(page, createdMealIds, createdMeals.map((meal) => meal?.date)); expect(mealsInOikos, 'created meals should be readable from the native Oikos meals API').toHaveLength(7); - expect(mealsInOikos.some((meal) => String(meal.title || '').includes('[E2E cleanup]'))).toBeTruthy(); + expect(mealsInOikos.some((meal) => String(meal.title || '').includes('[E2E cleanup] Guided wizard dinner'))).toBeTruthy(); expect(mealsInOikos.every((meal) => meal.meal_category), 'confirmed meals should preserve structured meal categories').toBeTruthy(); const leftoverMeal = mealsInOikos.find((meal) => meal.meal_category === 'leftovers'); expect(leftoverMeal?.leftover_from_meal_id, 'confirmed leftover meal should link to a source dish').toBeTruthy();