karby/tests/e2e.spec.js
cas 3422b09214 UX — verwijder menu-item Tussendoortjes uit hoofdnavigatie
- Remove navTussendoortjes button HTML, class toggle, and click listener
- Remaining nav items (Zoeken, Dagboek, Maaltijden) auto-distribute via flex:1
- Expose window.switchTab for test access (was IIFE-scoped)
- Update e2e tests: first test verifies button is gone, rest use
  page.evaluate(() => switchTab('tussendoortjes'))
- Update inspiratie.spec.js helper to use switchTab via evaluate
- Sync docker/index.html with index.html

Resolves: t_3c830823
2026-07-28 20:10:13 +02:00

763 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 tab: bottom-nav button is removed, tab accessible via switchTab', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1000);
// Verify the bottom-nav button is NOT present
await expect(page.locator('#navTussendoortjes')).not.toBeAttached();
// Verify the tab content still exists
await expect(page.locator('#tabTussendoortjes')).toBeAttached();
// Navigate via page.evaluate (simulating Vind inspiratie flow)
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(500);
await expect(page.locator('#tabTussendoortjes')).toBeVisible();
await expect(page.locator('.tussendoortjes-header')).toHaveText('Tussendoortjes');
await expect(page.locator('#rangeMin')).toBeVisible();
await expect(page.locator('#rangeMax')).toBeVisible();
});
test('tussendoortjes tab: default range shows suggestie cards', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1500);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(800);
// Default range is 15-20g — should show at least one suggestie card
const cards = page.locator('.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 tab: filter by range shows only matching totals', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1500);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(800);
// Set range to 10-15g — should show suggestions within this range
await page.locator('#rangeMax').click({ clickCount: 3 });
await page.locator('#rangeMax').fill('15');
await page.locator('#rangeMin').click({ clickCount: 3 });
await page.locator('#rangeMin').fill('10');
await page.waitForTimeout(500);
// Check that all visible totals are within 10-15g
const totals = page.locator('.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 tab: narrow range shows empty state', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1500);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(800);
// Set range to 0-1g — should show no suggestions
await page.locator('#rangeMax').click({ clickCount: 3 });
await page.locator('#rangeMax').fill('1');
await page.locator('#rangeMin').click({ clickCount: 3 });
await page.locator('#rangeMin').fill('0');
await page.waitForTimeout(500);
// Should show empty state
const cards = page.locator('.suggestie-card');
const cardCount = await cards.count();
expect(cardCount).toBe(0);
// Empty state text should be visible
await expect(page.locator('#tabTussendoortjes .empty-state-text')).toBeVisible();
});
test('tussendoortjes tab: warning label is present', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1000);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(500);
const warning = page.locator('.suggestie-warning');
await expect(warning).toBeVisible();
const text = await warning.textContent();
expect(text).toContain('NEVO');
expect(text).toContain('medisch');
});
test('tussendoortjes tab: each ingredient shows name, grams, and kh', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(2000);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(1000);
const ingredients = page.locator('.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 tab: range value 0 is not defaulted to 15', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1500);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(800);
// Set min to 0 — previously this would silently become 15 due to parseInt() || 15
await page.locator('#rangeMin').click({ clickCount: 3 });
await page.locator('#rangeMin').fill('0');
await page.waitForTimeout(300);
// Verify the input shows 0
await expect(page.locator('#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 tab: range 01 is interpreted correctly and shows empty state', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1500);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(800);
// Set range to 01
await page.locator('#rangeMax').click({ clickCount: 3 });
await page.locator('#rangeMax').fill('1');
await page.locator('#rangeMin').click({ clickCount: 3 });
await page.locator('#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('.suggestie-card');
const cardCount = await cards.count();
expect(cardCount).toBe(0);
await expect(page.locator('#tabTussendoortjes .empty-state-text')).toBeVisible();
});
test('tussendoortjes tab: range inputs have haptic metadata', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1000);
await page.evaluate(() => switchTab('tussendoortjes'));
await page.waitForTimeout(500);
// Both range inputs should have data-haptic attribute
await expect(page.locator('#rangeMin')).toHaveAttribute('data-haptic', 'normal');
await expect(page.locator('#rangeMax')).toHaveAttribute('data-haptic', 'normal');
});
});