- Haptic feedback: 8ms vibrate op elke button press (Android Vibration API) - Toast fix: slideUp/Down animatie ipv slideInRight (die de pagina verschoof) - 7 nieuwe e2e tests: steppers, modal-close animatie, tab-switching, date-nav, stagger-indices, button-transitions - Tests groen: 15/16 passed, 1 skipped
391 lines
16 KiB
JavaScript
391 lines
16 KiB
JavaScript
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();
|
|
});
|
|
|
|
});
|