- Verplaatst de bestaande tabTussendoortjes-inhoud naar een nieuwe inspiratieSheet bottom-sheet overlay (reuseert .modal-overlay, .modal-sheet, .modal-handle, backdrop-close en setupSwipeDown). - Klikken op 'Vind inspiratie' bij Tussendoor 1/2/3 opent nu de bottom sheet i.p.v. volledige tab-navigatie. - Sheet heeft X-knop, backdrop-click en swipe-down om te sluiten. - Haptics bij openen (10ms) en sluiten (6ms). - renderTussendoortjes() werkt zowel in tab als sheet context. - clearInspiratieContext() herstelt context ook als sheet open is. - Playwright-tests geüpdatet: verifiëren sheet open/sluit, titel, context, X/backdrop close, Dagboek blijft actief, geen tab-navigatie. - docker/index.html gesynchroniseerd.
789 lines
35 KiB
JavaScript
789 lines
35 KiB
JavaScript
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');
|
||
});
|
||
|
||
});
|