From 828b43c2603af3955ed4fdf2dfdc2f8315625010 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 14 May 2026 14:43:55 +0200 Subject: [PATCH] test: add meal studio 50 use-case e2e --- artifacts/oikos-meal-studio-50-use-cases.json | 1425 +++++++++++++++++ .../oikos-meal-studio-50-use-cases.spec.mjs | 296 ++++ 2 files changed, 1721 insertions(+) create mode 100644 artifacts/oikos-meal-studio-50-use-cases.json create mode 100644 tests/e2e/oikos-meal-studio-50-use-cases.spec.mjs diff --git a/artifacts/oikos-meal-studio-50-use-cases.json b/artifacts/oikos-meal-studio-50-use-cases.json new file mode 100644 index 0000000..8e05ba9 --- /dev/null +++ b/artifacts/oikos-meal-studio-50-use-cases.json @@ -0,0 +1,1425 @@ +{ + "startedAt": "2026-05-14T12:43:08.366Z", + "baseURL": "https://home.friborg.uk", + "summary": { + "total": 50, + "passed": 50, + "failed": 0, + "issueCount": 0 + }, + "issues": [], + "diagnostics": { + "consoleErrors": [ + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-+QLM0kgcBYVEt/7qfUCgpzrvhTqBauCzaIsHnsoiU98='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-y97di3J8rXKJFoeIf+5RSWeMs0TLhZs8y9ufpS+Gmes='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516' violates the following Content Security Policy directive: \"script-src 'self'\". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked.", + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-+QLM0kgcBYVEt/7qfUCgpzrvhTqBauCzaIsHnsoiU98='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-iILEDUk/sWh6TvpsMx0jHgvJ2n98AE+w8zuMOvS70X0='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-+QLM0kgcBYVEt/7qfUCgpzrvhTqBauCzaIsHnsoiU98='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Executing inline script violates the following Content Security Policy directive 'script-src 'self''. Either the 'unsafe-inline' keyword, a hash ('sha256-I50alSIYUCNYp9+jJpCE+lgBelv82DfL1vu1Au3J7tU='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.", + "error: Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516' violates the following Content Security Policy directive: \"script-src 'self'\". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked." + ], + "pageErrors": [], + "failedRequests": [ + "GET https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516 :: csp", + "GET https://static.cloudflareinsights.com/beacon.min.js/v8c78df7c7c0f484497ecbca7046644da1771523124516 :: csp" + ] + }, + "results": [ + { + "id": 1, + "name": "low_effort freezer quick", + "date": "2026-05-18", + "mode": "low_effort", + "modifiers": [ + "freezer", + "very_quick" + ], + "intent": "freezer", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 4, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Fryser", + "Meget hurtigt" + ] + }, + { + "id": 2, + "name": "low_effort leftovers quick", + "date": "2026-05-19", + "mode": "low_effort", + "modifiers": [ + "leftovers", + "very_quick" + ], + "intent": "leftovers", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Reste-bowl med sprødt grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Rester", + "Meget hurtigt" + ] + }, + { + "id": 3, + "name": "low_effort rugbrod", + "date": "2026-05-20", + "mode": "low_effort", + "modifiers": [ + "rugbrod" + ], + "intent": "quick", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Rugbrød/koldt" + ] + }, + { + "id": 4, + "name": "low_effort kids normal", + "date": "2026-05-21", + "mode": "low_effort", + "modifiers": [], + "intent": "easy", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt" + ] + }, + { + "id": 5, + "name": "regular no modifiers", + "date": "2026-05-22", + "mode": "regular", + "modifiers": [], + "intent": "regular", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 6, + "name": "regular cook extra", + "date": "2026-05-23", + "mode": "regular", + "modifiers": [ + "cook_extra" + ], + "intent": "batch", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Lav ekstra" + ] + }, + { + "id": 7, + "name": "regular leftovers", + "date": "2026-05-24", + "mode": "regular", + "modifiers": [ + "leftovers" + ], + "intent": "leftovers", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Reste-bowl med sprødt grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Rester" + ] + }, + { + "id": 8, + "name": "regular freezer", + "date": "2026-05-25", + "mode": "regular", + "modifiers": [ + "freezer" + ], + "intent": "freezer", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Fryser" + ] + }, + { + "id": 9, + "name": "regular very quick", + "date": "2026-05-26", + "mode": "regular", + "modifiers": [ + "very_quick" + ], + "intent": "quick", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Meget hurtigt" + ] + }, + { + "id": 10, + "name": "flexible guests", + "date": "2026-05-27", + "mode": "flexible", + "modifiers": [ + "guests" + ], + "intent": "guests", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Minestrone", + "Stor ovnret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster" + ] + }, + { + "id": 11, + "name": "flexible guests cook extra", + "date": "2026-05-28", + "mode": "flexible", + "modifiers": [ + "guests", + "cook_extra" + ], + "intent": "guests_batch", + "pass": true, + "issues": [], + "slotTitle": "Fiskefrikadeller med kartoffelsalat og rugbrød", + "cardTitles": [ + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Kyllingelasagne med salat og fuldkornspasta", + "Stor ovnret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster", + "Lav ekstra" + ] + }, + { + "id": 12, + "name": "flexible no kids", + "date": "2026-05-29", + "mode": "flexible", + "modifiers": [ + "no_kids" + ], + "intent": "adult", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Risotto", + "Voksen nudelskål med chili og grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Børnefri" + ] + }, + { + "id": 13, + "name": "flexible no kids eating out", + "date": "2026-05-30", + "mode": "flexible", + "modifiers": [ + "no_kids", + "eating_out" + ], + "intent": "eating_out", + "pass": true, + "issues": [], + "slotTitle": "Spiser ude", + "cardTitles": [ + "Spiser ude", + "Rester fra fryseren", + "Voksen takeaway eller café tæt på dagens rute" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Børnefri", + "Spiser ude" + ] + }, + { + "id": 14, + "name": "flexible eating out", + "date": "2026-05-31", + "mode": "flexible", + "modifiers": [ + "eating_out" + ], + "intent": "eating_out", + "pass": true, + "issues": [], + "slotTitle": "Spiser ude", + "cardTitles": [ + "Spiser ude", + "Rester fra fryseren", + "Takeaway eller café tæt på dagens rute" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Spiser ude" + ] + }, + { + "id": 15, + "name": "flexible freezer guests", + "date": "2026-05-18", + "mode": "flexible", + "modifiers": [ + "freezer", + "guests" + ], + "intent": "freezer_guests", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Gæstevenlig fryserret med salat og brød" + ], + "inventoryOptionCount": 4, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Fryser", + "Gæster" + ] + }, + { + "id": 16, + "name": "low effort guests conflict", + "date": "2026-05-19", + "mode": "low_effort", + "modifiers": [ + "guests" + ], + "intent": "conflict", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Minestrone", + "Stor ovnret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Gæster" + ] + }, + { + "id": 17, + "name": "low effort no kids", + "date": "2026-05-20", + "mode": "low_effort", + "modifiers": [ + "no_kids" + ], + "intent": "adult_quick", + "pass": true, + "issues": [], + "slotTitle": "Risotto", + "cardTitles": [ + "Risotto", + "Kyllingelasagne med salat og fuldkornspasta", + "Voksen nudelskål med chili og grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Børnefri" + ] + }, + { + "id": 18, + "name": "regular rugbrod kids", + "date": "2026-05-21", + "mode": "regular", + "modifiers": [ + "rugbrod" + ], + "intent": "cold", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Risotto", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Rugbrød/koldt" + ] + }, + { + "id": 19, + "name": "regular no kids cook extra", + "date": "2026-05-22", + "mode": "regular", + "modifiers": [ + "no_kids", + "cook_extra" + ], + "intent": "adult_batch", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Risotto", + "Voksen nudelskål med chili og grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Børnefri", + "Lav ekstra" + ] + }, + { + "id": 20, + "name": "flexible all special", + "date": "2026-05-23", + "mode": "flexible", + "modifiers": [ + "guests", + "cook_extra", + "freezer" + ], + "intent": "complex", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Gæstevenlig fryserret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster", + "Lav ekstra", + "Fryser" + ] + }, + { + "id": 21, + "name": "freezer only", + "date": "2026-05-24", + "mode": "regular", + "modifiers": [ + "freezer" + ], + "intent": "inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Fryser" + ] + }, + { + "id": 22, + "name": "leftovers only", + "date": "2026-05-25", + "mode": "regular", + "modifiers": [ + "leftovers" + ], + "intent": "inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Reste-bowl med sprødt grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Rester" + ] + }, + { + "id": 23, + "name": "eating out only", + "date": "2026-05-26", + "mode": "regular", + "modifiers": [ + "eating_out" + ], + "intent": "non_cooking", + "pass": true, + "issues": [], + "slotTitle": "Spiser ude", + "cardTitles": [ + "Spiser ude", + "Rester fra fryseren", + "Takeaway eller café tæt på dagens rute" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Spiser ude" + ] + }, + { + "id": 24, + "name": "cook extra only", + "date": "2026-05-27", + "mode": "regular", + "modifiers": [ + "cook_extra" + ], + "intent": "batch", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Lav ekstra" + ] + }, + { + "id": 25, + "name": "very quick only", + "date": "2026-05-28", + "mode": "regular", + "modifiers": [ + "very_quick" + ], + "intent": "quick", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Meget hurtigt" + ] + }, + { + "id": 26, + "name": "rugbrod very quick", + "date": "2026-05-29", + "mode": "low_effort", + "modifiers": [ + "rugbrod", + "very_quick" + ], + "intent": "cold_quick", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Rugbrød/koldt", + "Meget hurtigt" + ] + }, + { + "id": 27, + "name": "guests no kids", + "date": "2026-05-30", + "mode": "flexible", + "modifiers": [ + "guests", + "no_kids" + ], + "intent": "adult_guests", + "pass": true, + "issues": [], + "slotTitle": "Fiskefrikadeller med kartoffelsalat og rugbrød", + "cardTitles": [ + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Kyllingelasagne med salat og fuldkornspasta", + "Voksen nudelskål med chili og grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster", + "Børnefri" + ] + }, + { + "id": 28, + "name": "guests leftovers", + "date": "2026-05-31", + "mode": "flexible", + "modifiers": [ + "guests", + "leftovers" + ], + "intent": "leftover_guests", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Gæstevenlig fryserret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster", + "Rester" + ] + }, + { + "id": 29, + "name": "no kids freezer", + "date": "2026-05-18", + "mode": "flexible", + "modifiers": [ + "no_kids", + "freezer" + ], + "intent": "adult_inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Voksen fryser-tapas med chili og grønt" + ], + "inventoryOptionCount": 4, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Børnefri", + "Fryser" + ] + }, + { + "id": 30, + "name": "kids easy freezer", + "date": "2026-05-19", + "mode": "low_effort", + "modifiers": [ + "freezer" + ], + "intent": "kid_inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Fryser" + ] + }, + { + "id": 31, + "name": "busy monday", + "date": "2026-05-20", + "mode": "low_effort", + "modifiers": [ + "very_quick" + ], + "intent": "calendar_pressure", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Meget hurtigt" + ] + }, + { + "id": 32, + "name": "normal tuesday", + "date": "2026-05-21", + "mode": "regular", + "modifiers": [], + "intent": "baseline", + "pass": true, + "issues": [], + "slotTitle": "Burger", + "cardTitles": [ + "Burger", + "Pitabrød med falafel", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 33, + "name": "ella cooks thursday", + "date": "2026-05-22", + "mode": "regular", + "modifiers": [ + "very_quick" + ], + "intent": "kid_cook", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Meget hurtigt" + ] + }, + { + "id": 34, + "name": "friday guests", + "date": "2026-05-23", + "mode": "flexible", + "modifiers": [ + "guests" + ], + "intent": "weekend_guest", + "pass": true, + "issues": [], + "slotTitle": "Fiskefrikadeller med kartoffelsalat og rugbrød", + "cardTitles": [ + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Kyllingelasagne med salat og fuldkornspasta", + "Stor ovnret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster" + ] + }, + { + "id": 35, + "name": "saturday big cook", + "date": "2026-05-24", + "mode": "flexible", + "modifiers": [ + "cook_extra" + ], + "intent": "weekend_batch", + "pass": true, + "issues": [], + "slotTitle": "Fiskefrikadeller med kartoffelsalat og rugbrød", + "cardTitles": [ + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Burger", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Lav ekstra" + ] + }, + { + "id": 36, + "name": "sunday family regular", + "date": "2026-05-25", + "mode": "regular", + "modifiers": [], + "intent": "sunday", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 37, + "name": "rainy comfort fallback", + "date": "2026-05-26", + "mode": "low_effort", + "modifiers": [], + "intent": "weather", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt" + ] + }, + { + "id": 38, + "name": "warm grill flexible", + "date": "2026-05-27", + "mode": "flexible", + "modifiers": [], + "intent": "weather", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere" + ] + }, + { + "id": 39, + "name": "partial plan one day", + "date": "2026-05-28", + "mode": "regular", + "modifiers": [], + "intent": "partial", + "pass": true, + "issues": [], + "slotTitle": "Burger", + "cardTitles": [ + "Burger", + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 40, + "name": "two-day low effort streak", + "date": "2026-05-29", + "mode": "low_effort", + "modifiers": [ + "very_quick" + ], + "intent": "variety", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Meget hurtigt" + ] + }, + { + "id": 41, + "name": "avoid duplicate category", + "date": "2026-05-30", + "mode": "regular", + "modifiers": [], + "intent": "variety", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Burger", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 42, + "name": "manual freezer unknown", + "date": "2026-05-31", + "mode": "low_effort", + "modifiers": [ + "freezer" + ], + "intent": "manual_inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Fryser" + ] + }, + { + "id": 43, + "name": "planned leftovers style", + "date": "2026-05-18", + "mode": "regular", + "modifiers": [ + "leftovers" + ], + "intent": "planned_leftovers", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Reste-bowl med sprødt grønt" + ], + "inventoryOptionCount": 4, + "modulatorLabels": [ + "sæson:spring", + "normal belastning", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag", + "Rester" + ] + }, + { + "id": 44, + "name": "eating out still has cards", + "date": "2026-05-19", + "mode": "flexible", + "modifiers": [ + "eating_out" + ], + "intent": "cards", + "pass": true, + "issues": [], + "slotTitle": "Spiser ude", + "cardTitles": [ + "Spiser ude", + "Rester fra fryseren", + "Takeaway eller café tæt på dagens rute" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Spiser ude" + ] + }, + { + "id": 45, + "name": "new suggestion distinct", + "date": "2026-05-20", + "mode": "regular", + "modifiers": [], + "intent": "cards", + "pass": true, + "issues": [], + "slotTitle": "Kyllingelasagne med salat og fuldkornspasta", + "cardTitles": [ + "Kyllingelasagne med salat og fuldkornspasta", + "Risotto", + "Rugbrødsbræt med æg, grønt og rester" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 46, + "name": "rare suggestion present", + "date": "2026-05-21", + "mode": "regular", + "modifiers": [], + "intent": "cards", + "pass": true, + "issues": [], + "slotTitle": "Pitabrød med falafel", + "cardTitles": [ + "Pitabrød med falafel", + "Risotto", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 47, + "name": "known suggestion present", + "date": "2026-05-22", + "mode": "regular", + "modifiers": [], + "intent": "cards", + "pass": true, + "issues": [], + "slotTitle": "Risotto", + "cardTitles": [ + "Risotto", + "Burger", + "Nordisk kyllingebakke med grønt" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Hverdag" + ] + }, + { + "id": 48, + "name": "modulator labels visible", + "date": "2026-05-23", + "mode": "flexible", + "modifiers": [ + "guests", + "cook_extra" + ], + "intent": "ui", + "pass": true, + "issues": [], + "slotTitle": "Fiskefrikadeller med kartoffelsalat og rugbrød", + "cardTitles": [ + "Fiskefrikadeller med kartoffelsalat og rugbrød", + "Kyllingelasagne med salat og fuldkornspasta", + "Stor ovnret med salat og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "rolig dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Plads til mere", + "Gæster", + "Lav ekstra" + ] + }, + { + "id": 49, + "name": "inventory options on freezer", + "date": "2026-05-24", + "mode": "low_effort", + "modifiers": [ + "freezer" + ], + "intent": "inventory", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Fryser" + ] + }, + { + "id": 50, + "name": "reservation-compatible day", + "date": "2026-05-25", + "mode": "low_effort", + "modifiers": [ + "freezer", + "very_quick" + ], + "intent": "inventory_reserve", + "pass": true, + "issues": [], + "slotTitle": "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "cardTitles": [ + "Rester fra Taxonomi middag E2E-mp3550hd-0-k7tp", + "Rester fra Taxonomi middag E2E-mp352nuq-0-ah0w", + "Fryser-tapas med grønt og brød" + ], + "inventoryOptionCount": 2, + "modulatorLabels": [ + "sæson:spring", + "nem/travl dag", + "Jimmi som kok", + "udendørs/grill muligt", + "variation fra start", + "Nemt", + "Fryser", + "Meget hurtigt" + ] + } + ] +} \ No newline at end of file diff --git a/tests/e2e/oikos-meal-studio-50-use-cases.spec.mjs b/tests/e2e/oikos-meal-studio-50-use-cases.spec.mjs new file mode 100644 index 0000000..add50b1 --- /dev/null +++ b/tests/e2e/oikos-meal-studio-50-use-cases.spec.mjs @@ -0,0 +1,296 @@ +import { expect, test } from '@playwright/test'; +import fs from 'node:fs'; + +const username = process.env.OIKOS_E2E_USERNAME; +const password = process.env.OIKOS_E2E_PASSWORD_FILE + ? fs.readFileSync(process.env.OIKOS_E2E_PASSWORD_FILE, 'utf8').trim() + : process.env.OIKOS_E2E_PASSWORD; + +const MODE_LABELS = { + low_effort: 'Nemt', + regular: 'Hverdag', + flexible: 'Plads til mere', +}; + +const MODIFIER_LABELS = { + freezer: 'Fryser', + leftovers: 'Rester', + eating_out: 'Spiser ude', + guests: 'Gæster', + no_kids: 'Børnefri', + cook_extra: 'Lav ekstra', + rugbrod: 'Rugbrød/koldt', + very_quick: 'Meget hurtigt', +}; + +function isoAdd(start, offset) { + const date = new Date(`${start}T12:00:00Z`); + date.setUTCDate(date.getUTCDate() + offset); + return date.toISOString().slice(0, 10); +} + +function nextMonday() { + const date = new Date(); + date.setUTCHours(12, 0, 0, 0); + const day = date.getUTCDay() || 7; + date.setUTCDate(date.getUTCDate() + (8 - day)); + return date.toISOString().slice(0, 10); +} + +async function dismissOnboardingIfPresent(page) { + const skip = page.getByRole('button', { name: /^(Skip|Spring over|Senere|Close|Luk)$/i }).first(); + try { if (await skip.isVisible({ timeout: 1500 })) await skip.click(); } catch {} +} + +async function login(page) { + if (!username || !password) throw new Error('Missing OIKOS_E2E_USERNAME and password env.'); + await page.goto('/login', { waitUntil: 'domcontentloaded' }); + await page.locator('#username').fill(username); + await page.locator('#password').fill(password); + const loginResponsePromise = page.waitForResponse((res) => res.url().includes('/api/v1/auth/login')); + await page.locator('#login-btn').click(); + const loginResponse = await loginResponsePromise; + if (!loginResponse.ok()) throw new Error(`Login failed HTTP ${loginResponse.status()}`); + await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 20_000 }); + await expect(page.locator('#main-content')).toBeVisible(); + await dismissOnboardingIfPresent(page); +} + +function buildUseCases(start) { + const base = [ + ['low_effort freezer quick', 'low_effort', ['freezer', 'very_quick'], 'freezer'], + ['low_effort leftovers quick', 'low_effort', ['leftovers', 'very_quick'], 'leftovers'], + ['low_effort rugbrod', 'low_effort', ['rugbrod'], 'quick'], + ['low_effort kids normal', 'low_effort', [], 'easy'], + ['regular no modifiers', 'regular', [], 'regular'], + ['regular cook extra', 'regular', ['cook_extra'], 'batch'], + ['regular leftovers', 'regular', ['leftovers'], 'leftovers'], + ['regular freezer', 'regular', ['freezer'], 'freezer'], + ['regular very quick', 'regular', ['very_quick'], 'quick'], + ['flexible guests', 'flexible', ['guests'], 'guests'], + ['flexible guests cook extra', 'flexible', ['guests', 'cook_extra'], 'guests_batch'], + ['flexible no kids', 'flexible', ['no_kids'], 'adult'], + ['flexible no kids eating out', 'flexible', ['no_kids', 'eating_out'], 'eating_out'], + ['flexible eating out', 'flexible', ['eating_out'], 'eating_out'], + ['flexible freezer guests', 'flexible', ['freezer', 'guests'], 'freezer_guests'], + ['low effort guests conflict', 'low_effort', ['guests'], 'conflict'], + ['low effort no kids', 'low_effort', ['no_kids'], 'adult_quick'], + ['regular rugbrod kids', 'regular', ['rugbrod'], 'cold'], + ['regular no kids cook extra', 'regular', ['no_kids', 'cook_extra'], 'adult_batch'], + ['flexible all special', 'flexible', ['guests', 'cook_extra', 'freezer'], 'complex'], + ['freezer only', 'regular', ['freezer'], 'inventory'], + ['leftovers only', 'regular', ['leftovers'], 'inventory'], + ['eating out only', 'regular', ['eating_out'], 'non_cooking'], + ['cook extra only', 'regular', ['cook_extra'], 'batch'], + ['very quick only', 'regular', ['very_quick'], 'quick'], + ['rugbrod very quick', 'low_effort', ['rugbrod', 'very_quick'], 'cold_quick'], + ['guests no kids', 'flexible', ['guests', 'no_kids'], 'adult_guests'], + ['guests leftovers', 'flexible', ['guests', 'leftovers'], 'leftover_guests'], + ['no kids freezer', 'flexible', ['no_kids', 'freezer'], 'adult_inventory'], + ['kids easy freezer', 'low_effort', ['freezer'], 'kid_inventory'], + ['busy monday', 'low_effort', ['very_quick'], 'calendar_pressure'], + ['normal tuesday', 'regular', [], 'baseline'], + ['ella cooks thursday', 'regular', ['very_quick'], 'kid_cook'], + ['friday guests', 'flexible', ['guests'], 'weekend_guest'], + ['saturday big cook', 'flexible', ['cook_extra'], 'weekend_batch'], + ['sunday family regular', 'regular', [], 'sunday'], + ['rainy comfort fallback', 'low_effort', [], 'weather'], + ['warm grill flexible', 'flexible', [], 'weather'], + ['partial plan one day', 'regular', [], 'partial'], + ['two-day low effort streak', 'low_effort', ['very_quick'], 'variety'], + ['avoid duplicate category', 'regular', [], 'variety'], + ['manual freezer unknown', 'low_effort', ['freezer'], 'manual_inventory'], + ['planned leftovers style', 'regular', ['leftovers'], 'planned_leftovers'], + ['eating out still has cards', 'flexible', ['eating_out'], 'cards'], + ['new suggestion distinct', 'regular', [], 'cards'], + ['rare suggestion present', 'regular', [], 'cards'], + ['known suggestion present', 'regular', [], 'cards'], + ['modulator labels visible', 'flexible', ['guests', 'cook_extra'], 'ui'], + ['inventory options on freezer', 'low_effort', ['freezer'], 'inventory'], + ['reservation-compatible day', 'low_effort', ['freezer', 'very_quick'], 'inventory_reserve'], + ]; + return base.map(([name, mode, modifiers, intent], idx) => ({ + id: idx + 1, + name, + date: isoAdd(start, idx % 14), + mode, + modifiers, + intent, + })); +} + +function analyzeSlot(useCase, slot) { + const issues = []; + const mods = slot?.context?.modulators || {}; + const cards = slot?.suggestionCards || []; + if (!slot) issues.push('No slot returned for use case date.'); + if (mods.dayMode !== useCase.mode) issues.push(`Expected dayMode ${useCase.mode}, got ${mods.dayMode}.`); + for (const modifier of useCase.modifiers) { + if (!(mods.manualModifiers || []).includes(modifier)) issues.push(`Missing manual modifier ${modifier}.`); + } + if (cards.length !== 3) issues.push(`Expected exactly 3 suggestion cards, got ${cards.length}.`); + const titles = cards.map((card) => card.title).filter(Boolean); + if (new Set(titles).size !== titles.length) issues.push(`Suggestion card titles are not distinct: ${titles.join(' | ')}`); + if (useCase.modifiers.includes('eating_out') && !/spiser ude|takeaway|café/i.test(`${slot.title} ${titles.join(' ')}`)) issues.push('Eating-out use case did not produce an intentional eating-out/takeaway option.'); + if (useCase.modifiers.includes('freezer') && !(slot.inventoryOptions || []).length) issues.push('Freezer use case did not expose inventoryOptions.'); + if (useCase.modifiers.includes('freezer') && !/fryser|freezer/i.test(`${slot.title} ${titles.join(' ')}`)) issues.push('Freezer use case did not show freezer-oriented wording.'); + if (useCase.modifiers.includes('leftovers') && !/rester|leftover|fryser/i.test(`${slot.title} ${titles.join(' ')}`)) issues.push('Leftovers use case did not show leftover-oriented wording.'); + if (useCase.modifiers.includes('guests') && !/gæst|guest|stor|salat|ovnret/i.test(`${slot.reason} ${titles.join(' ')}`)) issues.push('Guests use case did not explain/scaffold guest fit.'); + if (useCase.modifiers.includes('no_kids') && !/børnefri|voksen|adult|chili/i.test(`${slot.reason} ${titles.join(' ')}`)) issues.push('No-kids use case did not open adult/no-kids suggestions.'); + if (useCase.mode === 'low_effort' && !mods.easyDay) issues.push('Low-effort use case was not marked as easyDay.'); + if (!Array.isArray(mods.labels) || mods.labels.length < 2) issues.push('Modulator labels are too sparse for user inspection.'); + return issues; +} + +async function attachReport(testInfo, report) { + await testInfo.attach('oikos-meal-studio-50-use-cases.json', { + body: JSON.stringify(report, null, 2), + contentType: 'application/json', + }); + const markdown = [ + '# Oikos Meal Plan Studio — 50-use-case E2E verification', + '', + `Run: ${report.startedAt}`, + `Base URL: ${report.baseURL}`, + `Passed: ${report.summary.passed}/${report.summary.total}`, + `Issues: ${report.summary.issueCount}`, + '', + '## Issues / defects / shortcomings', + ...(report.issues.length ? report.issues.map((issue) => `- UC${String(issue.id).padStart(2, '0')} ${issue.name}: ${issue.issue}`) : ['- None recorded.']), + '', + '## Use-case results', + ...report.results.map((row) => `- ${row.pass ? '✅' : '⚠️'} UC${String(row.id).padStart(2, '0')} ${row.name} — ${row.mode} [${row.modifiers.join(', ') || 'none'}] → ${row.slotTitle || 'no slot'}`), + ].join('\n'); + await testInfo.attach('oikos-meal-studio-50-use-cases.md', { body: markdown, contentType: 'text/markdown' }); +} + +test.describe('Oikos Meal Plan Studio comprehensive 50-use-case verification', () => { + test('50 realistic meal-planning use cases across API, UI, inventory, and mobile flow', async ({ page }, testInfo) => { + test.setTimeout(240_000); + const diagnostics = { consoleErrors: [], pageErrors: [], failedRequests: [] }; + page.on('console', (msg) => { if (['error', 'warning'].includes(msg.type())) diagnostics.consoleErrors.push(`${msg.type()}: ${msg.text()}`); }); + page.on('pageerror', (err) => diagnostics.pageErrors.push(err.stack || err.message)); + page.on('requestfailed', (req) => diagnostics.failedRequests.push(`${req.method()} ${req.url()} :: ${req.failure()?.errorText || 'failed'}`)); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.addInitScript(() => localStorage.setItem('oikos-onboarded', '1')); + await login(page); + + const startedAt = new Date().toISOString(); + const baseURL = testInfo.project.use.baseURL || process.env.OIKOS_E2E_BASE_URL || 'https://home.friborg.uk'; + const start = nextMonday(); + const useCases = buildUseCases(start); + const results = []; + const issues = []; + + await test.step('health and served asset markers', async () => { + const health = await page.request.get('/ai/health'); + expect(health.ok()).toBeTruthy(); + const widget = await page.request.get('/ai/widget.js'); + expect(widget.ok()).toBeTruthy(); + const widgetText = await widget.text(); + for (const marker of ['assist-day-setup', 'assist-meal-options', 'assist-leftover-inventory', 'applySuggestionCard', 'reserveInventoryForDate']) { + if (!widgetText.includes(marker)) issues.push({ id: 0, name: 'served widget marker', issue: `Missing widget marker ${marker}` }); + } + const css = await page.request.get('/ai/assist.css'); + const cssText = await css.text(); + for (const marker of ['Three-card meal picker slice', 'Leftover/freezer inventory picker slice']) { + if (!cssText.includes(marker)) issues.push({ id: 0, name: 'served CSS marker', issue: `Missing CSS marker ${marker}` }); + } + }); + + await test.step('inventory API add/reserve/discard smoke', async () => { + const uniqueTitle = `E2E fryser portion ${Date.now()}`; + const create = await page.request.post('/ai/api/meal-plan/leftovers', { data: { title: uniqueTitle, servings: 3, location: 'freezer', expiresAt: isoAdd(start, 30), notes: 'E2E verification item' } }); + const created = await create.json(); + if (!create.ok() || !created.item?.id) issues.push({ id: 0, name: 'inventory create', issue: `Could not create inventory item: HTTP ${create.status()}` }); + else { + const reserve = await page.request.patch('/ai/api/meal-plan/leftovers', { data: { id: created.item.id, patch: { status: 'reserved', reservedForDate: start } } }); + if (!reserve.ok()) issues.push({ id: 0, name: 'inventory reserve', issue: `Could not reserve inventory item: HTTP ${reserve.status()}` }); + const discard = await page.request.patch('/ai/api/meal-plan/leftovers', { data: { id: created.item.id, patch: { status: 'discarded' } } }); + if (!discard.ok()) issues.push({ id: 0, name: 'inventory cleanup', issue: `Could not discard E2E inventory item: HTTP ${discard.status()}` }); + } + }); + + for (const useCase of useCases) { + await test.step(`UC${String(useCase.id).padStart(2, '0')} ${useCase.name}`, async () => { + const response = await page.request.post('/ai/api/meal-plan/generate', { + data: { + startDate: useCase.date, + endDate: useCase.date, + dayConfigs: [{ date: useCase.date, mode: useCase.mode, modifiers: useCase.modifiers }], + }, + }); + let payload = null; + if (!response.ok()) { + const issue = `Generation failed HTTP ${response.status()}`; + issues.push({ id: useCase.id, name: useCase.name, issue }); + results.push({ ...useCase, pass: false, issues: [issue] }); + return; + } + payload = await response.json(); + const slot = payload.slots?.[0]; + const rowIssues = analyzeSlot(useCase, slot); + for (const issue of rowIssues) issues.push({ id: useCase.id, name: useCase.name, issue }); + results.push({ + ...useCase, + pass: rowIssues.length === 0, + issues: rowIssues, + slotTitle: slot?.title, + cardTitles: (slot?.suggestionCards || []).map((card) => card.title), + inventoryOptionCount: slot?.inventoryOptions?.length || 0, + modulatorLabels: slot?.context?.modulators?.labels || [], + }); + }); + } + + await test.step('mobile UI smoke for day chips, 3 cards, and inventory panel', async () => { + await page.goto('/meals', { waitUntil: 'domcontentloaded' }); + await dismissOnboardingIfPresent(page); + const host = page.locator('[data-oikos-meal-plan-studio]'); + await expect(host).toBeVisible({ timeout: 20_000 }); + await host.locator('[data-assist-studio-generate]').first().click(); + await expect(host.locator('.assist-day-setup')).toBeVisible({ timeout: 30_000 }); + await expect(host.locator('.assist-meal-options').first()).toBeVisible({ timeout: 20_000 }); + await expect(host.locator('.assist-leftover-inventory')).toBeVisible({ timeout: 20_000 }); + const dayModeButtons = await host.locator('[data-assist-day-mode]').count(); + const modifierButtons = await host.locator('[data-assist-day-modifier]').count(); + const optionCards = await host.locator('[data-assist-suggestion-card]').count(); + const overflow = await page.evaluate(() => ({ innerWidth: window.innerWidth, scrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth })); + if (dayModeButtons < 21) issues.push({ id: 0, name: 'mobile day mode UI', issue: `Expected day mode buttons for 7 days, got ${dayModeButtons}` }); + if (modifierButtons < 56) issues.push({ id: 0, name: 'mobile modifier UI', issue: `Expected modifier buttons for 7 days, got ${modifierButtons}` }); + if (optionCards < 21) issues.push({ id: 0, name: 'mobile suggestion cards', issue: `Expected 3 option cards for 7 days, got ${optionCards}` }); + if (overflow.scrollWidth > overflow.innerWidth + 12) issues.push({ id: 0, name: 'mobile overflow', issue: `Horizontal overflow: ${JSON.stringify(overflow)}` }); + + const firstTitleBefore = await host.locator('[data-assist-studio-title="0"]').inputValue(); + const secondCard = host.locator('[data-assist-suggestion-card]').nth(1); + if (await secondCard.count()) { + await secondCard.click(); + const firstTitleAfter = await host.locator('[data-assist-studio-title="0"]').inputValue(); + if (firstTitleBefore === firstTitleAfter) issues.push({ id: 0, name: 'suggestion card selection', issue: 'Clicking second suggestion card did not update first slot title.' }); + } + }); + + const report = { + startedAt, + baseURL, + summary: { + total: useCases.length, + passed: results.filter((row) => row.pass).length, + failed: results.filter((row) => !row.pass).length, + issueCount: issues.length, + }, + issues, + diagnostics, + results, + }; + + await attachReport(testInfo, report); + fs.mkdirSync('artifacts', { recursive: true }); + fs.writeFileSync('artifacts/oikos-meal-studio-50-use-cases.json', JSON.stringify(report, null, 2)); + + expect(results).toHaveLength(50); + expect(diagnostics.pageErrors, 'No browser page errors').toEqual([]); + expect(issues, `Recorded issues:\n${issues.map((i) => `UC${i.id} ${i.name}: ${i.issue}`).join('\n')}`).toEqual([]); + }); +});