test: cover guided meal studio wizard
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user