test: cover guided meal studio wizard

This commit is contained in:
OpenClaw Bot
2026-05-14 18:29:38 +02:00
parent 828b43c260
commit 0bbbc1d154
+59 -26
View File
@@ -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();