const { test, expect } = require('@playwright/test'); const BASE = 'https://eetdagboek.vantwout.dev'; test.describe('Karby Eetdagboek', () => { test('page loads with correct title and elements', async ({ page }) => { await page.goto(BASE); 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(BASE); 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(BASE); 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(BASE); 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(BASE); 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(BASE); 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(BASE); 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(BASE); 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(BASE); await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); // Open add-to-meal modal (hier zitten de steppers) 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); // Eerste stepper heeft count=1 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(BASE); 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; } // + op tweede stepper await steppers.nth(1).locator('.portie-stepper-plus').click(); await page.waitForTimeout(100); // Eerste moet 0 zijn (gereset) await expect(steppers.first().locator('.portie-stepper-count')).toHaveText('0'); // Tweede moet 1 zijn 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(BASE); 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(); // Default count = 1 await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('1'); // Minus klikken → 0 await firstStepper.locator('.portie-stepper-minus').click(); await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('0'); // Minus moet nu disabled zijn (niet negatief) await expect(firstStepper.locator('.portie-stepper-minus')).toBeDisabled(); }); test('portion stepper: active class na plus/minus toggles', async ({ page }) => { await page.goto(BASE); 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 is pas aanwezig na updatePortionFromSteppers (triggered door +/-) // Eerst: plus klikken om active te krijgen await firstStepper.locator('.portie-stepper-plus').click(); await page.waitForTimeout(100); await expect(firstStepper).toHaveClass(/active/); // Minus naar 0 → active verliezen await firstStepper.locator('.portie-stepper-minus').click(); await expect(firstStepper).not.toHaveClass(/active/); }); // ---------- Modal animaties ---------- test('modal close animation: closing class applied during dismiss', async ({ page }) => { await page.goto(BASE); 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(BASE); await page.locator('#settingsBtn').click(); await page.waitForTimeout(100); const overlay = page.locator('#settingsModal'); await expect(overlay).toBeVisible(); await expect(overlay).not.toHaveClass(/closing/); // .modal-overlay heeft animation: fadeIn const overlayAnim = await overlay.evaluate(el => getComputedStyle(el).animationName); expect(overlayAnim).toBe('fadeIn'); // .modal-sheet heeft animation: slideUp const sheet = overlay.locator('.modal-sheet'); const sheetAnim = await sheet.evaluate(el => getComputedStyle(el).animationName); expect(sheetAnim).toBe('slideUp'); // Sluiten voor opschoning await page.locator('#settingsModal .modal-close').click(); await page.waitForTimeout(500); }); test('tab switching: switching class verschijnt tijdens crossfade', async ({ page }) => { await page.goto(BASE); 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 is empty by default', async ({ page }) => { await page.goto(BASE); // Zoek + open detail await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); // Toevoegen aan eetmoment await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); // Terug naar dagboek (sluit addMealModal eerst) await page.locator('#addMealModal .modal-close').click(); await page.waitForTimeout(300); // Sluit detail modal await page.locator('#detailModal .modal-close').click(); await page.waitForTimeout(300); // Naar dagboek tab await page.locator('#navDagboek').click(); await page.waitForTimeout(600); // Zoek save-meal btn en klik const saveBtn = page.locator('.save-meal-btn'); if (await saveBtn.count() === 0) { test.skip('geen items in dagboek'); return; } await saveBtn.first().click(); await page.waitForTimeout(500); const nameInput = page.locator('#mealNameInput'); await expect(nameInput).toBeVisible({ timeout: 5000 }); // Naamveld moet leeg zijn await expect(nameInput).toHaveValue(''); // Annuleren 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(BASE); // Zoek + open detail await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); // Toevoegen aan eetmoment await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); // Sluit addMealModal await page.locator('#addMealModal .modal-close').click(); await page.waitForTimeout(300); // Sluit detail modal await page.locator('#detailModal .modal-close').click(); await page.waitForTimeout(300); // Naar dagboek await page.locator('#navDagboek').click(); await page.waitForTimeout(600); // Opslaan als maaltijd const saveBtn = page.locator('.save-meal-btn'); if (await saveBtn.count() === 0) { test.skip('geen items in dagboek'); return; } await saveBtn.first().click(); await page.waitForTimeout(500); const nameInput = page.locator('#mealNameInput'); await expect(nameInput).toBeVisible({ timeout: 5000 }); await expect(nameInput).toHaveValue(''); // Opslaan zonder naam await page.locator('#mealNameSave').click(); await page.waitForTimeout(300); // Modal moet nog open zijn (niet gesloten) await expect(nameInput).toBeVisible(); // Input border moet rood zijn (validation) const borderColor = await nameInput.evaluate(el => getComputedStyle(el).borderColor); const rgb = borderColor.match(/\d+/g).map(Number); expect(rgb[0]).toBeGreaterThan(150); // R > 150 expect(rgb[1]).toBeLessThan(100); // G < 100 expect(rgb[2]).toBeLessThan(100); // B < 100 // Annuleren 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(BASE); await page.locator('#navDagboek').click(); await page.waitForTimeout(500); const initialDay = await page.locator('#dayLabel').textContent(); // Next day (tweede .day-nav-btn) 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); // Today reset 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(BASE); 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(BASE); const btn = page.locator('button').first(); 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(BASE); // Stap 1: Zoek voedingsmiddel await page.locator('.search-input').fill('brood'); await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); // Stap 2: Voeg toe aan eetmoment await page.locator('#detailAddBtn').click(); await page.waitForTimeout(500); // Sluit addMealModal await page.locator('#addMealModal .modal-close').click(); await page.waitForTimeout(300); // Sluit detail modal (anders overlay intercepteert nav knoppen) await page.locator('#detailModal .modal-close').click(); await page.waitForTimeout(300); // Stap 3: Naar dagboek await page.locator('#navDagboek').click(); await page.waitForTimeout(600); // Stap 4: Opslaan als maaltijd const saveBtn = page.locator('.save-meal-btn'); if (await saveBtn.count() === 0) { test.skip('geen items in dagboek — kan geen maaltijd opslaan'); return; } await saveBtn.first().click(); await page.waitForTimeout(500); // Stap 5: Naam invullen en opslaan 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); // De meal wordt opgeslagen, toast verschijnt // Stap 6: Ga terug naar dagboek om meal-group te zien await page.locator('#navDagboek').click(); await page.waitForTimeout(800); // Stap 7: Zoek de meal-group const mealGroup = page.locator('.eetmoment-meal-group').first(); await expect(mealGroup).toBeVisible({ timeout: 5000 }); // Stap 8: Items-wrap begint collapsed (hidden class) const itemsWrap = mealGroup.locator('.eetmoment-meal-items-wrap'); await expect(itemsWrap).toHaveClass(/hidden/); // Stap 9: Klik op header om uit te klappen const header = mealGroup.locator('.eetmoment-meal-header'); await header.click(); await page.waitForTimeout(300); await expect(itemsWrap).not.toHaveClass(/hidden/); // Chevron moet naar boven wijzen const chevronUp = header.locator('i.fa-chevron-up'); await expect(chevronUp).toBeVisible(); // Stap 10: Klik opnieuw om in te klappen await header.click(); await page.waitForTimeout(300); await expect(itemsWrap).toHaveClass(/hidden/); const chevronDown = header.locator('i.fa-chevron-down'); await expect(chevronDown).toBeVisible(); }); });