const { test, expect } = require('@playwright/test'); test.describe('Karby Eetdagboek', () => { test('page loads with correct title and elements', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle('Karby'); await expect(page.locator('.search-input')).toBeVisible(); await expect(page.locator('.cat-btn').first()).toBeVisible(); await expect(page.locator('#navZoeken')).toBeVisible(); await expect(page.locator('#navDagboek')).toBeVisible(); await expect(page.locator('#navMaaltijden')).toBeVisible(); }); test('search returns results and opens detail modal', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('aardappel'); await page.waitForTimeout(800); const results = page.locator('.food-item'); await expect(results.first()).toBeVisible({ timeout: 5000 }); await results.first().click(); await expect(page.locator('#detailModal')).not.toHaveClass(/hidden/, { timeout: 5000 }); await page.locator('#detailModal .modal-close').click(); await expect(page.locator('#detailModal')).toHaveClass(/hidden/, { timeout: 3000 }); }); test('category filters work', async ({ page }) => { await page.goto('/'); await page.locator('.cat-btn').filter({ hasText: 'Brood' }).click(); await page.waitForTimeout(500); const activeBtn = page.locator('.cat-btn.active'); await expect(activeBtn).toBeVisible(); }); test('add item to diary via eetmoment flow', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await expect(page.locator('#detailAddBtn')).toBeVisible({ timeout: 3000 }); }); test('diary tab shows date navigation', async ({ page }) => { await page.goto('/'); await page.locator('#navDagboek').click(); await page.waitForTimeout(500); await expect(page.locator('.day-navigator')).toBeVisible({ timeout: 3000 }); await expect(page.locator('.day-nav-btn').first()).toBeVisible(); await expect(page.locator('.day-nav-today')).toBeVisible(); }); test('settings modal opens', async ({ page }) => { await page.goto('/'); await page.locator('#settingsBtn').click(); await page.waitForTimeout(500); await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/, { timeout: 3000 }); await page.locator('#settingsModal .modal-close').click(); await expect(page.locator('#settingsModal')).toHaveClass(/hidden/, { timeout: 2000 }); }); test('maaltijden tab shows saved meals section', async ({ page }) => { await page.goto('/'); await page.locator('#navMaaltijden').click(); await page.waitForTimeout(500); await expect(page.locator('#tabMaaltijden')).toBeVisible(); }); test('contrast: key elements have visible colors', async ({ page }) => { await page.goto('/'); const navBtns = page.locator('.nav-btn'); const count = await navBtns.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const color = await navBtns.nth(i).evaluate(el => getComputedStyle(el).color); expect(color).not.toBe('rgba(0, 0, 0, 0)'); } }); // ========== NIEUWE TESTS — portie-steppers, animaties, maaltijden, datumkiezer, meal-groups ========== // ---------- Stepper: basis ---------- test('portion stepper: steppers appear in addMealModal with first count=1', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); const steppers = page.locator('.portie-stepper'); await expect(steppers.first()).toBeVisible({ timeout: 3000 }); expect(await steppers.count()).toBeGreaterThan(0); await expect(steppers.first().locator('.portie-stepper-count')).toHaveText('1'); }); test('portion stepper: plus op tweede activeert die en reset eerste naar 0', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); const steppers = page.locator('.portie-stepper'); const count = await steppers.count(); if (count < 2) { test.skip(); return; } await steppers.nth(1).locator('.portie-stepper-plus').click(); await page.waitForTimeout(100); await expect(steppers.first().locator('.portie-stepper-count')).toHaveText('0'); await expect(steppers.nth(1).locator('.portie-stepper-count')).toHaveText('1'); }); test('portion stepper: minus verlaagt count naar 0 en wordt dan disabled', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); const firstStepper = page.locator('.portie-stepper').first(); await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('1'); await firstStepper.locator('.portie-stepper-minus').click(); await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('0'); await expect(firstStepper.locator('.portie-stepper-minus')).toBeDisabled(); }); test('portion stepper: active class na plus/minus toggles', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); const firstStepper = page.locator('.portie-stepper').first(); // active class na updatePortionFromSteppers triggeren via plus await firstStepper.locator('.portie-stepper-plus').click(); await page.waitForTimeout(100); await expect(firstStepper).toHaveClass(/active/); // Minus 2×: 2→1→0, dan active kwijt await firstStepper.locator('.portie-stepper-minus').click(); await page.waitForTimeout(50); await firstStepper.locator('.portie-stepper-minus').click(); await page.waitForTimeout(50); await expect(firstStepper).not.toHaveClass(/active/); }); // ---------- Modal animaties ---------- test('modal close animation: closing class applied during dismiss', async ({ page }) => { await page.goto('/'); await page.locator('#settingsBtn').click(); await page.waitForTimeout(500); await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/); await page.locator('#settingsModal .modal-close').click(); await page.waitForTimeout(50); await expect(page.locator('#settingsModal')).toHaveClass(/closing/); await page.waitForTimeout(600); await expect(page.locator('#settingsModal')).toHaveClass(/hidden/); }); test('modal open animation: fadeIn en slideUp CSS animations gestart', async ({ page }) => { await page.goto('/'); await page.locator('#settingsBtn').click(); await page.waitForTimeout(100); const overlay = page.locator('#settingsModal'); await expect(overlay).toBeVisible(); await expect(overlay).not.toHaveClass(/closing/); const overlayAnim = await overlay.evaluate(el => getComputedStyle(el).animationName); expect(overlayAnim).toBe('fadeIn'); const sheet = overlay.locator('.modal-sheet'); const sheetAnim = await sheet.evaluate(el => getComputedStyle(el).animationName); expect(sheetAnim).toBe('slideUp'); await page.locator('#settingsModal .modal-close').click(); await page.waitForTimeout(500); }); test('tab switching: switching class verschijnt tijdens crossfade', async ({ page }) => { await page.goto('/'); await page.locator('#navDagboek').click(); await page.waitForTimeout(100); const tabEl = page.locator('#tabDagboek'); await expect(tabEl).toHaveClass(/switching/); await page.waitForTimeout(500); await expect(tabEl).toBeVisible(); }); // ---------- Maaltijden ---------- test('meal save: name input pre-filled with smart suggestion', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); // Item toevoegen via addMealSubmit → sluit modal + gaat naar dagboek await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Add a second item (meal requires 2+ ingredients) await page.locator('#navZoeken').click(); await page.waitForTimeout(300); await page.locator('.search-input').fill('kaas'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Add a second item (meal requires 2+ ingredients) await page.locator('#navZoeken').click(); await page.waitForTimeout(300); await page.locator('.search-input').fill('kaas'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Check 2 items and show save button via evaluate await page.evaluate(() => { const cbs = document.querySelectorAll('.item-checkbox'); if (cbs[0]) cbs[0].checked = true; if (cbs[1]) cbs[1].checked = true; const btn = document.querySelector('.save-meal-footer-btn'); if (btn) { btn.classList.remove('footer-hidden'); btn.textContent = '⭐ Opslaan 2 items als maaltijd'; } }); await page.waitForTimeout(300); const saveBtn = page.locator('.save-meal-footer-btn'); await expect(saveBtn.first()).toBeVisible({ timeout: 5000 }); // Force click through intercepting parent elements await saveBtn.first().dispatchEvent('click'); await page.waitForTimeout(500); const nameInput = page.locator('#mealNameInput'); await expect(nameInput).toBeVisible({ timeout: 5000 }); // Smart suggestion should pre-fill with a non-empty name await expect(nameInput).not.toHaveValue(''); await page.locator('#mealNameCancel').click(); await page.waitForTimeout(300); }); test('meal save: empty name toont rode rand en modal blijft open', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Add a second item (meal requires 2+ ingredients) await page.locator('#navZoeken').click(); await page.waitForTimeout(300); await page.locator('.search-input').fill('kaas'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Check 2 items and show save button via evaluate await page.evaluate(() => { const cbs = document.querySelectorAll('.item-checkbox'); if (cbs[0]) cbs[0].checked = true; if (cbs[1]) cbs[1].checked = true; const btn = document.querySelector('.save-meal-footer-btn'); if (btn) { btn.classList.remove('footer-hidden'); btn.textContent = '⭐ Opslaan 2 items als maaltijd'; } }); await page.waitForTimeout(300); const saveBtn = page.locator('.save-meal-footer-btn'); await expect(saveBtn.first()).toBeVisible({ timeout: 5000 }); // Force click through intercepting parent elements await saveBtn.first().dispatchEvent('click'); await page.waitForTimeout(500); const nameInput = page.locator('#mealNameInput'); await expect(nameInput).toBeVisible({ timeout: 5000 }); // Clear the smart-suggested name to test empty validation await nameInput.fill(''); // Opslaan zonder naam → validatiefout await page.locator('#mealNameSave').click(); await page.waitForTimeout(300); await expect(nameInput).toBeVisible(); const borderColor = await nameInput.evaluate(el => getComputedStyle(el).borderColor); const rgb = borderColor.match(/\d+/g).map(Number); expect(rgb[0]).toBeGreaterThan(150); expect(rgb[1]).toBeLessThan(100); expect(rgb[2]).toBeLessThan(100); await page.locator('#mealNameCancel').click(); await page.waitForTimeout(300); }); // ---------- Datumkiezer ---------- test('date navigator: next/prev verandert datum en today reset', async ({ page }) => { await page.goto('/'); await page.locator('#navDagboek').click(); await page.waitForTimeout(500); const initialDay = await page.locator('#dayLabel').textContent(); const nextBtn = page.locator('.day-nav-btn').nth(1); await nextBtn.click(); await page.waitForTimeout(300); const nextDay = await page.locator('#dayLabel').textContent(); expect(nextDay).not.toBe(initialDay); await page.locator('.day-nav-today').click(); await page.waitForTimeout(300); const todayDay = await page.locator('#dayLabel').textContent(); expect(todayDay).toBe(initialDay); }); // ---------- Zoek animaties ---------- test('zoektab: food items krijgen staggered animation indices', async ({ page }) => { await page.goto('/'); await page.locator('.search-input').fill('k'); await page.waitForTimeout(800); const items = page.locator('.food-item'); const count = await items.count(); if (count >= 2) { const i0 = await items.first().evaluate(el => el.style.getPropertyValue('--i')); expect(i0.trim()).toBe('0'); const i1 = await items.nth(1).evaluate(el => el.style.getPropertyValue('--i')); expect(i1.trim()).toBe('1'); } }); test('button press: buttons hebben transition transform bij indrukken', async ({ page }) => { await page.goto('/', { waitUntil: 'networkidle' }); const btn = page.locator('button').first(); await expect(btn).toBeVisible({ timeout: 5000 }); const transition = await btn.evaluate(el => getComputedStyle(el).transition); expect(transition).toBeTruthy(); expect(transition).toContain('transform'); }); // ---------- Meal-group uitklappen in dagboek ---------- test('meal-group uitklappen: saved meal toggles expand/collapse in dagboek', async ({ page }) => { await page.goto('/'); // Stap 1: Zoek voedingsmiddel en voeg toe aan dagboek await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Add a second item (meal requires 2+ ingredients) await page.locator('#navZoeken').click(); await page.waitForTimeout(300); await page.locator('.search-input').fill('kaas'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Add a second item (meal requires 2+ ingredients) await page.locator('#navZoeken').click(); await page.waitForTimeout(300); await page.locator('.search-input').fill('kaas'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Check 2 items and show save button via evaluate await page.evaluate(() => { const cbs = document.querySelectorAll('.item-checkbox'); if (cbs[0]) cbs[0].checked = true; if (cbs[1]) cbs[1].checked = true; const btn = document.querySelector('.save-meal-footer-btn'); if (btn) { btn.classList.remove('footer-hidden'); btn.textContent = '⭐ Opslaan 2 items als maaltijd'; } }); await page.waitForTimeout(300); // Stap 2: Sla op als maaltijd const saveBtn = page.locator('.save-meal-footer-btn'); await expect(saveBtn.first()).toBeVisible({ timeout: 5000 }); // Force click through intercepting parent elements await saveBtn.first().dispatchEvent('click'); await page.waitForTimeout(500); const nameInput = page.locator('#mealNameInput'); await expect(nameInput).toBeVisible({ timeout: 5000 }); await nameInput.fill('Test ontbijt'); await page.locator('#mealNameSave').click(); await page.waitForTimeout(600); // Stap 3: Ga naar maaltijden tab om de maaltijd toe te voegen aan dagboek await page.locator('#navMaaltijden').click(); await page.waitForTimeout(600); // Stap 4: Klik "Toevoegen aan dagboek" op de opgeslagen maaltijd const mealCardAdd = page.locator('.meal-card-add').first(); await expect(mealCardAdd).toBeVisible({ timeout: 5000 }); await mealCardAdd.click(); await page.waitForTimeout(500); // Stap 5: Klik op een eetmoment-optie in de modal (bv. Ontbijt) const mealOption = page.locator('.meal-add-option').first(); await expect(mealOption).toBeVisible({ timeout: 3000 }); await mealOption.click(); await page.waitForTimeout(600); // Sluit eventuele resteerde modals door op de overlay te klikken const anyOverlay = page.locator('.modal-overlay:not(.hidden)').first(); if (await anyOverlay.isVisible().catch(() => false)) { await anyOverlay.click({ position: { x: 10, y: 10 } }); await page.waitForTimeout(300); } // Stap 6: Ga naar dagboek om de meal-group te zien await page.locator('#navDagboek').click(); await page.waitForTimeout(800); // Stap 7: Zoek meal-group const mealGroup = page.locator('.eetmoment-meal-group').first(); await expect(mealGroup).toBeVisible({ timeout: 5000 }); // Stap 8: Items-wrap begint collapsed const itemsWrap = mealGroup.locator('.eetmoment-meal-items-wrap'); // Stap 8: Items-wrap start zichtbaar (geen hidden class) await expect(itemsWrap).not.toHaveClass(/hidden|collapsed/); // Stap 9: Klik toggle om in te klappen → collapsed class toegevoegd const toggle = mealGroup.locator('[data-action="toggle-meal"]'); await toggle.click(); await page.waitForTimeout(300); await expect(itemsWrap).toHaveClass(/collapsed/); const chevronDown = toggle.locator('i.fa-chevron-down'); await expect(chevronDown).toBeVisible(); // Stap 10: Klik opnieuw om uit te klappen → hidden weg await toggle.click(); await page.waitForTimeout(300); await expect(itemsWrap).not.toHaveClass(/hidden|collapsed/); const chevronUp = toggle.locator('i.fa-chevron-up'); await expect(chevronUp).toBeVisible(); }); // ---------- Confirm + delete dialogs ---------- test('confirm modal: deletion dialog shows design-conforme buttons', async ({ page }) => { await page.goto('/'); // Add item + save as meal from search tab await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Navigate to diary, save as meal await page.locator('#navDagboek').click(); await page.waitForTimeout(600); const saveBtn = page.locator('.save-meal-footer-btn'); if (await saveBtn.count() === 0) { test.skip(); return; } await saveBtn.first().click(); await page.waitForTimeout(500); await page.locator('#mealNameInput').fill('Test'); await page.locator('#mealNameSave').click(); await page.waitForTimeout(600); // Go to maaltijden and delete await page.locator('#navMaaltijden').click(); await page.waitForTimeout(600); // Open first meal edit await page.locator('.meal-card-main').first().click(); await page.waitForTimeout(500); const deleteBtn = page.locator('#mealEditDelete'); if (await deleteBtn.isVisible().catch(() => false)) { await deleteBtn.click(); await page.waitForTimeout(300); await expect(page.locator('#confirmOk')).toBeVisible({ timeout: 3000 }); await expect(page.locator('#confirmCancel')).toBeVisible(); await page.locator('#confirmCancel').click(); } }); test('portie-edit dialog: single ingredient portion change saves and closes', async ({ page }) => { // Add a loose ingredient to the diary first await page.goto('/'); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); // Click "Toevoegen aan eetmoment" → opens addMealModal await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); // Confirm adding to diary (default portie=100g, default eetmoment) await page.locator('#addMealSubmit').click(); await page.waitForTimeout(800); // Should now be on dagboek tab // Find the portion display for a single ingredient (no data-mi attribute) const portionSpan = page.locator('[data-action="edit-portie"]:not([data-mi])').first(); await expect(portionSpan).toBeVisible({ timeout: 5000 }); await portionSpan.click(); await page.waitForTimeout(500); // Verify the portion edit dialog opened const editInput = page.locator('#portieEditInput'); await expect(editInput).toBeVisible({ timeout: 3000 }); // Verify preview shows initial carbs const preview = page.locator('#portieEditPreview'); await expect(preview).toBeVisible(); // Change the portion to 200g await editInput.fill('200'); await editInput.dispatchEvent('input'); await page.waitForTimeout(300); // Click save await page.locator('#portieSaveBtn').click(); await page.waitForTimeout(500); // Dialog should be gone (removed from DOM after closing animation) await expect(page.locator('#portieEditInput')).not.toBeAttached({ timeout: 3000 }); }); test('infinite scroll: only first batch rendered, sentinel triggers more', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); // wait for data load // Clear any search query to see all items const clearBtn = page.locator('#searchClear'); if (await clearBtn.isVisible().catch(() => false)) { await clearBtn.click(); await page.waitForTimeout(500); } // Count initially rendered food items — should be ≤ SEARCH_PAGE_SIZE (200) const initialCount = await page.locator('.food-item').count(); expect(initialCount).toBeLessThanOrEqual(200); expect(initialCount).toBeGreaterThan(0); // Sentinel should exist (there are more than 200 items) await expect(page.locator('#searchSentinel')).toBeAttached({ timeout: 3000 }); // Scroll sentinel into view to trigger next batch await page.locator('#searchSentinel').scrollIntoViewIfNeeded(); await page.waitForTimeout(1200); // Now more items should be rendered (up to 2 batches of 200) const afterScrollCount = await page.locator('.food-item').count(); expect(afterScrollCount).toBeGreaterThan(initialCount); expect(afterScrollCount).toBeLessThanOrEqual(400); // max 2 batches total (200 + 200) }); test('infinite scroll: search query resets pagination', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); // Scroll down a bit to load more const sentinel = page.locator('#searchSentinel'); if (await sentinel.isVisible().catch(() => false)) { await sentinel.scrollIntoViewIfNeeded(); await page.waitForTimeout(800); } const beforeSearch = await page.locator('.food-item').count(); // Now type a search query — should reset to first batch (≤ 200 items) await page.locator('.search-input').fill('aardappel'); await page.waitForTimeout(800); const afterSearch = await page.locator('.food-item').count(); expect(afterSearch).toBeLessThanOrEqual(200); // Should be fewer than before (filtered), unless we hadn't scrolled far if (beforeSearch > 200) { expect(afterSearch).toBeLessThan(beforeSearch); } }); // ========== TUSSENDOORTJES (via inspiratie-flow, nav-knop verwijderd) ========== test('tussendoortjes inspiratie: bottom sheet opent via Vind inspiratie vanuit elk Tussendoor-moment', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1000); // Verify the bottom-nav button is NOT present await expect(page.locator('#navTussendoortjes')).not.toBeAttached(); // Open dagboek await page.click('#navDagboek'); await page.waitForTimeout(600); // Click first Vind inspiratie button const btn = page.locator('.vind-inspiratie-btn').first(); await expect(btn).toBeVisible(); await btn.click(); await page.waitForTimeout(600); // Sheet should open await expect(page.locator('#inspiratieSheet')).toBeVisible(); await expect(page.locator('#inspiratieSheet')).not.toHaveClass(/hidden/); // Sheet title shows inspiration context await expect(page.locator('#inspiratieSheetTitle')).toContainText('Inspiratie voor'); // Range controls are in the sheet await expect(page.locator('#inspiratieSheet #rangeMin')).toBeVisible(); await expect(page.locator('#inspiratieSheet #rangeMax')).toBeVisible(); // Dagboek stays active (no tab switch) await expect(page.locator('#navDagboek')).toHaveClass(/active/); }); test('tussendoortjes inspiratie: default range shows suggestie cards in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); await page.click('#navDagboek'); await page.waitForTimeout(600); // Click first Vind inspiratie to open sheet await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(800); // Default range is 15-20g — should show at least one suggestie card const cards = page.locator('#inspiratieSheet .suggestie-card'); const count = await cards.count(); expect(count).toBeGreaterThanOrEqual(1); // Each card should have a name, total, ingredients, and preparation const firstCard = cards.first(); await expect(firstCard.locator('.suggestie-card-naam')).toBeVisible(); await expect(firstCard.locator('.suggestie-card-totaal')).toBeVisible(); await expect(firstCard.locator('.suggestie-ingredient').first()).toBeVisible(); await expect(firstCard.locator('.suggestie-bereiding')).toBeVisible(); }); test('tussendoortjes inspiratie: filter by range in sheet shows only matching totals', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(800); // Set range to 10-15g — should show suggestions within this range await page.locator('#inspiratieSheet #rangeMax').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMax').fill('15'); await page.locator('#inspiratieSheet #rangeMin').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMin').fill('10'); await page.waitForTimeout(500); // Check that all visible totals are within 10-15g const totals = page.locator('#inspiratieSheet .suggestie-card-totaal'); const totalCount = await totals.count(); for (let i = 0; i < totalCount; i++) { const text = await totals.nth(i).textContent(); const val = parseFloat(text); expect(val).toBeGreaterThanOrEqual(10); expect(val).toBeLessThanOrEqual(15); } }); test('tussendoortjes inspiratie: narrow range shows empty state in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(800); // Set range to 0-1g — should show no suggestions await page.locator('#inspiratieSheet #rangeMax').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMax').fill('1'); await page.locator('#inspiratieSheet #rangeMin').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMin').fill('0'); await page.waitForTimeout(500); // Should show empty state const cards = page.locator('#inspiratieSheet .suggestie-card'); const cardCount = await cards.count(); expect(cardCount).toBe(0); // Empty state text should be visible in the sheet await expect(page.locator('#inspiratieSheet .inspiratie-empty')).toBeVisible(); }); test('tussendoortjes inspiratie: warning label is present in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1000); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(600); const warning = page.locator('#inspiratieSheet .suggestie-warning'); await expect(warning).toBeVisible(); const text = await warning.textContent(); expect(text).toContain('NEVO'); expect(text).toContain('medisch'); }); test('tussendoortjes inspiratie: each ingredient shows name, grams, and kh in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(2000); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(1000); const ingredients = page.locator('#inspiratieSheet .suggestie-ingredient'); const count = await ingredients.count(); expect(count).toBeGreaterThanOrEqual(3); // at least 3 total ingredient rows across all cards // Each ingredient row should have name, grams, and kh value const firstIng = ingredients.first(); await expect(firstIng.locator('.suggestie-ingredient-naam')).toBeVisible(); await expect(firstIng.locator('.suggestie-ingredient-gram')).toBeVisible(); const gramText = await firstIng.locator('.suggestie-ingredient-gram').textContent(); expect(gramText).toMatch(/\d+g/); // e.g. "175g" await expect(firstIng.locator('.suggestie-ingredient-kh')).toBeVisible(); const khText = await firstIng.locator('.suggestie-ingredient-kh').textContent(); expect(khText).toMatch(/\d+[.,]?\d* g kh/); // e.g. "6.7 g kh" }); test('tussendoortjes inspiratie: range value 0 is not defaulted to 15 in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(800); // Set min to 0 — previously this would silently become 15 due to parseInt() || 15 await page.locator('#inspiratieSheet #rangeMin').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMin').fill('0'); await page.waitForTimeout(300); // Verify the input shows 0 await expect(page.locator('#inspiratieSheet #rangeMin')).toHaveValue('0'); // Verify that the app correctly interprets 0 (not falling back to 15) const parseResult = await page.evaluate(() => { const el = document.getElementById('rangeMin'); const v = el.value; const raw = parseInt(v, 10); const oldFallback = raw || 15; // old broken approach const newFallback = isNaN(raw) ? 15 : raw; // corrected approach return { value: v, raw, oldFallback, newFallback }; }); expect(parseResult.raw).toBe(0); // parseInt('0') → 0 expect(parseResult.oldFallback).toBe(15); // 0 || 15 → 15 (the bug) expect(parseResult.newFallback).toBe(0); // isNaN(0) ? 15 : 0 → 0 (fixed) }); test('tussendoortjes inspiratie: range 0–1 is interpreted correctly and shows empty state in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(800); // Set range to 0–1 await page.locator('#inspiratieSheet #rangeMax').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMax').fill('1'); await page.locator('#inspiratieSheet #rangeMin').click({ clickCount: 3 }); await page.locator('#inspiratieSheet #rangeMin').fill('0'); await page.waitForTimeout(500); // Verify the actual parsed values via berekenReceptTotalen — min should be 0, not 15 const rangeValues = await page.evaluate(() => { const rawMin = parseInt(document.getElementById('rangeMin').value, 10); const rawMax = parseInt(document.getElementById('rangeMax').value, 10); return { rangeMin: isNaN(rawMin) ? 15 : rawMin, rangeMax: isNaN(rawMax) ? 20 : rawMax, }; }); expect(rangeValues.rangeMin).toBe(0); expect(rangeValues.rangeMax).toBe(1); // Should show empty state const cards = page.locator('#inspiratieSheet .suggestie-card'); const cardCount = await cards.count(); expect(cardCount).toBe(0); await expect(page.locator('#inspiratieSheet .inspiratie-empty')).toBeVisible(); }); test('tussendoortjes inspiratie: range inputs have haptic metadata in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1000); await page.click('#navDagboek'); await page.waitForTimeout(600); await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(500); // Both range inputs should have data-haptic attribute await expect(page.locator('#inspiratieSheet #rangeMin')).toHaveAttribute('data-haptic', 'normal'); await expect(page.locator('#inspiratieSheet #rangeMax')).toHaveAttribute('data-haptic', 'normal'); }); });