Compare commits
No commits in common. "develop" and "main" have entirely different histories.
5 changed files with 194 additions and 3600 deletions
1455
docker/index.html
1455
docker/index.html
File diff suppressed because it is too large
Load diff
1455
index.html
1455
index.html
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,11 @@
|
|||
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('/');
|
||||
await page.goto(BASE);
|
||||
await expect(page).toHaveTitle('Karby');
|
||||
await expect(page.locator('.search-input')).toBeVisible();
|
||||
await expect(page.locator('.cat-btn').first()).toBeVisible();
|
||||
|
|
@ -13,7 +15,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('search returns results and opens detail modal', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('aardappel');
|
||||
await page.waitForTimeout(800);
|
||||
const results = page.locator('.food-item');
|
||||
|
|
@ -25,7 +27,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('category filters work', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.cat-btn').filter({ hasText: 'Brood' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
const activeBtn = page.locator('.cat-btn.active');
|
||||
|
|
@ -33,7 +35,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('add item to diary via eetmoment flow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -42,7 +44,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('diary tab shows date navigation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#navDagboek').click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('.day-navigator')).toBeVisible({ timeout: 3000 });
|
||||
|
|
@ -51,7 +53,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('settings modal opens', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#settingsBtn').click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/, { timeout: 3000 });
|
||||
|
|
@ -60,14 +62,14 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('maaltijden tab shows saved meals section', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
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('/');
|
||||
await page.goto(BASE);
|
||||
const navBtns = page.locator('.nav-btn');
|
||||
const count = await navBtns.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
|
@ -82,7 +84,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Stepper: basis ----------
|
||||
|
||||
test('portion stepper: steppers appear in addMealModal with first count=1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -96,7 +98,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('portion stepper: plus op tweede activeert die en reset eerste naar 0', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -113,7 +115,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('portion stepper: minus verlaagt count naar 0 en wordt dan disabled', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -128,7 +130,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('portion stepper: active class na plus/minus toggles', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -151,7 +153,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Modal animaties ----------
|
||||
|
||||
test('modal close animation: closing class applied during dismiss', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#settingsBtn').click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/);
|
||||
|
|
@ -163,7 +165,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('modal open animation: fadeIn en slideUp CSS animations gestart', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#settingsBtn').click();
|
||||
await page.waitForTimeout(100);
|
||||
const overlay = page.locator('#settingsModal');
|
||||
|
|
@ -179,7 +181,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('tab switching: switching class verschijnt tijdens crossfade', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#navDagboek').click();
|
||||
await page.waitForTimeout(100);
|
||||
const tabEl = page.locator('#tabDagboek');
|
||||
|
|
@ -191,7 +193,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Maaltijden ----------
|
||||
|
||||
test('meal save: name input pre-filled with smart suggestion', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -246,7 +248,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('meal save: empty name toont rode rand en modal blijft open', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -300,7 +302,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Datumkiezer ----------
|
||||
|
||||
test('date navigator: next/prev verandert datum en today reset', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('#navDagboek').click();
|
||||
await page.waitForTimeout(500);
|
||||
const initialDay = await page.locator('#dayLabel').textContent();
|
||||
|
|
@ -318,7 +320,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Zoek animaties ----------
|
||||
|
||||
test('zoektab: food items krijgen staggered animation indices', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.locator('.search-input').fill('k');
|
||||
await page.waitForTimeout(800);
|
||||
const items = page.locator('.food-item');
|
||||
|
|
@ -332,7 +334,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('button press: buttons hebben transition transform bij indrukken', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
await page.goto(BASE, { waitUntil: 'networkidle' });
|
||||
const btn = page.locator('button').first();
|
||||
await expect(btn).toBeVisible({ timeout: 5000 });
|
||||
const transition = await btn.evaluate(el => getComputedStyle(el).transition);
|
||||
|
|
@ -343,7 +345,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
// ---------- Meal-group uitklappen in dagboek ----------
|
||||
|
||||
test('meal-group uitklappen: saved meal toggles expand/collapse in dagboek', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
// Stap 1: Zoek voedingsmiddel en voeg toe aan dagboek
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
|
|
@ -424,25 +426,25 @@ test.describe('Karby Eetdagboek', () => {
|
|||
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();
|
||||
// Stap 9: Klik header om in te klappen → collapsed class toegevoegd
|
||||
const header = mealGroup.locator('.eetmoment-meal-header');
|
||||
await header.click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(itemsWrap).toHaveClass(/collapsed/);
|
||||
const chevronDown = toggle.locator('i.fa-chevron-down');
|
||||
const chevronDown = header.locator('i.fa-chevron-down');
|
||||
await expect(chevronDown).toBeVisible();
|
||||
// Stap 10: Klik opnieuw om uit te klappen → hidden weg
|
||||
await toggle.click();
|
||||
await header.click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(itemsWrap).not.toHaveClass(/hidden|collapsed/);
|
||||
const chevronUp = toggle.locator('i.fa-chevron-up');
|
||||
const chevronUp = header.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('/');
|
||||
await page.goto(BASE);
|
||||
// Add item + save as meal from search tab
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
|
|
@ -480,7 +482,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
|
||||
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.goto(BASE);
|
||||
await page.locator('.search-input').fill('brood');
|
||||
await page.waitForTimeout(800);
|
||||
await page.locator('.food-item').first().click();
|
||||
|
|
@ -523,7 +525,7 @@ test.describe('Karby Eetdagboek', () => {
|
|||
});
|
||||
|
||||
test('infinite scroll: only first batch rendered, sentinel triggers more', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.waitForTimeout(1500); // wait for data load
|
||||
|
||||
// Clear any search query to see all items
|
||||
|
|
@ -533,26 +535,26 @@ test.describe('Karby Eetdagboek', () => {
|
|||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Count initially rendered food items — should be ≤ SEARCH_PAGE_SIZE (200)
|
||||
// Count initially rendered food items — should be ≤ SEARCH_PAGE_SIZE (50)
|
||||
const initialCount = await page.locator('.food-item').count();
|
||||
expect(initialCount).toBeLessThanOrEqual(200);
|
||||
expect(initialCount).toBeLessThanOrEqual(50);
|
||||
expect(initialCount).toBeGreaterThan(0);
|
||||
|
||||
// Sentinel should exist (there are more than 200 items)
|
||||
// Sentinel should exist (there are more than 50 items)
|
||||
await expect(page.locator('#searchSentinel')).toBeAttached({ timeout: 3000 });
|
||||
|
||||
// Scroll sentinel into view to trigger next batch
|
||||
// Scroll sentinel into view to trigger next batches (now loads 3 at once)
|
||||
await page.locator('#searchSentinel').scrollIntoViewIfNeeded();
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
// Now more items should be rendered (up to 2 batches of 200)
|
||||
// Now more items should be rendered (up to 3 batches of 50)
|
||||
const afterScrollCount = await page.locator('.food-item').count();
|
||||
expect(afterScrollCount).toBeGreaterThan(initialCount);
|
||||
expect(afterScrollCount).toBeLessThanOrEqual(400); // max 2 batches total (200 + 200)
|
||||
expect(afterScrollCount).toBeLessThanOrEqual(200); // max 4 batches total (50 + 3×50)
|
||||
});
|
||||
|
||||
test('infinite scroll: search query resets pagination', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.goto(BASE);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Scroll down a bit to load more
|
||||
|
|
@ -564,226 +566,16 @@ test.describe('Karby Eetdagboek', () => {
|
|||
|
||||
const beforeSearch = await page.locator('.food-item').count();
|
||||
|
||||
// Now type a search query — should reset to first batch (≤ 200 items)
|
||||
// Now type a search query — should reset to ≤ 50 items
|
||||
await page.locator('.search-input').fill('aardappel');
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
const afterSearch = await page.locator('.food-item').count();
|
||||
expect(afterSearch).toBeLessThanOrEqual(200);
|
||||
expect(afterSearch).toBeLessThanOrEqual(50);
|
||||
// Should be fewer than before (filtered), unless we hadn't scrolled far
|
||||
if (beforeSearch > 200) {
|
||||
if (beforeSearch > 50) {
|
||||
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');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Regressietest: eetmoment-labels (icon + naam) zichtbaar in dagboek.
|
||||
*
|
||||
* Achtergrond (kaart t_a1d49160): "Eetmoment-labels niet zichtbaar in dagboek —
|
||||
* icon+naam tekst valt weg". Niet reproduceerbaar tegen lokale app noch live;
|
||||
* template/CSS zijn sinds de initiële commit correct. Deze test borgt dat alle
|
||||
* zes eetmomentkaarten hun icon + naam tonen, niet transparant en niet
|
||||
* afgeknipt — voor lege én gevulde kaarten, op mobiel én desktop.
|
||||
*
|
||||
* Determinisme: de dagboek-data wordt per scenario geseed via localStorage
|
||||
* (addInitScript) en er wordt gewacht tot de fadeIn-stagger-animatie klaar is
|
||||
* (opacity === 1) voordat er geassert wordt. Computed styles, geen screenshots.
|
||||
*/
|
||||
|
||||
const MOMENTS = [
|
||||
{ id: 'Ontbijt', name: 'Ontbijt', icon: '🥐' },
|
||||
{ id: 'Tussendoor 1', name: 'Tussendoor 1', icon: '☕' },
|
||||
{ id: 'Lunch', name: 'Lunch', icon: '🥪' },
|
||||
{ id: 'Tussendoor 2', name: 'Tussendoor 2', icon: '🍎' },
|
||||
{ id: 'Avondeten', name: 'Avondeten', icon: '🍲' },
|
||||
{ id: 'Tussendoor 3', name: 'Tussendoor 3', icon: '🍪' },
|
||||
];
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date();
|
||||
const p = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
||||
}
|
||||
|
||||
function buildDiaryJson(filled) {
|
||||
const day = {};
|
||||
for (const m of MOMENTS) {
|
||||
day[m.id] = filled
|
||||
? [{ item: { naam: 'Volkoren brood', kh: '50.0', n: '1' }, portie: 30 }]
|
||||
: [];
|
||||
}
|
||||
return JSON.stringify({ [todayStr()]: day });
|
||||
}
|
||||
|
||||
/** Seed dagboek-data in een verse context en open het dagboek-tabblad. */
|
||||
async function openDiary(page, filled) {
|
||||
await page.addInitScript((json) => {
|
||||
localStorage.setItem('koolhydraat-dagboek', json);
|
||||
}, buildDiaryJson(filled));
|
||||
await page.goto('/');
|
||||
await page.locator('#navDagboek').click();
|
||||
|
||||
// Wacht tot alle 6 kaarten gerenderd zijn én de fadeIn-stagger (0.35s +
|
||||
// maximaal 5 * 50ms delay) volledig is afgelopen — anders is opacity nog 0.
|
||||
await expect(page.locator('.eetmoment-card')).toHaveCount(6, { timeout: 5000 });
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.locator('.eetmoment-card').last().evaluate((el) => getComputedStyle(el).opacity)
|
||||
, { timeout: 5000 })
|
||||
.toBe('1');
|
||||
}
|
||||
|
||||
/** Assert per kaart: icon+naam aanwezig, zichtbaar, niet transparant, niet afgeknipt. */
|
||||
async function expectAllLabelsVisible(page) {
|
||||
const cards = page.locator('.eetmoment-card');
|
||||
expect(await cards.count()).toBe(6);
|
||||
|
||||
for (let i = 0; i < MOMENTS.length; i++) {
|
||||
const m = MOMENTS[i];
|
||||
const card = cards.nth(i);
|
||||
const header = card.locator('.eetmoment-header');
|
||||
const icon = card.locator('.eetmoment-icon');
|
||||
const name = card.locator('.eetmoment-name');
|
||||
|
||||
// Template: juiste moment-id, icon en naam aanwezig
|
||||
await expect(header).toHaveAttribute('data-moment', m.id);
|
||||
await expect(icon).toHaveText(m.icon, { timeout: 3000 });
|
||||
await expect(name).toHaveText(m.name, { timeout: 3000 });
|
||||
|
||||
// Computed style: zichtbaar + niet transparant + niet afgeknipt
|
||||
for (const el of [icon, name]) {
|
||||
const cs = await el.evaluate((node) => {
|
||||
const style = getComputedStyle(node);
|
||||
const r = node.getBoundingClientRect();
|
||||
const cardR = node.closest('.eetmoment-card').getBoundingClientRect();
|
||||
const alpha = (style.color.match(/rgba?\(([^)]+)\)/) || [])[1];
|
||||
const a = alpha ? parseFloat(alpha.split(',')[3] ?? '1') : 1;
|
||||
return {
|
||||
display: style.display,
|
||||
visibility: style.visibility,
|
||||
opacity: parseFloat(style.opacity),
|
||||
colorAlpha: Number.isNaN(a) ? 1 : a,
|
||||
w: r.width,
|
||||
h: r.height,
|
||||
insideCard: r.bottom <= cardR.bottom + 1 && r.top >= cardR.top - 1,
|
||||
};
|
||||
});
|
||||
|
||||
expect(cs.display, `${m.id}: ${el === icon ? 'icon' : 'naam'} display`).not.toBe('none');
|
||||
expect(cs.visibility, `${m.id}: ${el === icon ? 'icon' : 'naam'} visibility`).toBe('visible');
|
||||
expect(cs.opacity, `${m.id}: ${el === icon ? 'icon' : 'naam'} opacity`).toBeGreaterThan(0.99);
|
||||
expect(cs.colorAlpha, `${m.id}: ${el === icon ? 'icon' : 'naam'} transparant`).toBeGreaterThan(0);
|
||||
expect(cs.w, `${m.id}: ${el === icon ? 'icon' : 'naam'} breedte 0`).toBeGreaterThan(0);
|
||||
expect(cs.h, `${m.id}: ${el === icon ? 'icon' : 'naam'} hoogte 0`).toBeGreaterThan(0);
|
||||
expect(cs.insideCard, `${m.id}: ${el === icon ? 'icon' : 'naam'} afgeknipt`).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Eetmoment-labels — icon+naam zichtbaar (regressie t_a1d49160)', () => {
|
||||
for (const filled of [false, true]) {
|
||||
const label = filled ? 'gevuld' : 'leeg';
|
||||
test(`${label}: alle zes labels zichtbaar, niet transparant/afgeknipt (mobiel)`, async ({ page }) => {
|
||||
await openDiary(page, filled);
|
||||
await expectAllLabelsVisible(page);
|
||||
});
|
||||
|
||||
test.describe(`desktop viewport (${label})`, () => {
|
||||
test.use({ viewport: { width: 1280, height: 800 } });
|
||||
test('alle zes labels zichtbaar, niet transparant/afgeknipt', async ({ page }) => {
|
||||
await openDiary(page, filled);
|
||||
await expectAllLabelsVisible(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* E2E tests for the "Vind inspiratie" feature on Tussendoor 1/2/3
|
||||
*
|
||||
* The feature opens a bottom sheet (inspiratieSheet) instead of a full tab/page.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - The app must be served at the configured baseURL
|
||||
* - Run `npx playwright test` from the project root
|
||||
* - Or `LOCAL_TEST=http://localhost:8080 npx playwright test` for local file serving
|
||||
*/
|
||||
|
||||
// Helper: wait for DOM to be fully interactive after navigation
|
||||
async function waitForApp(page) {
|
||||
// Wait for the app shell to be ready
|
||||
await page.waitForSelector('#app', { timeout: 15000 });
|
||||
// Ensure the IIFE has initialized by checking for bottom nav
|
||||
await page.waitForSelector('#navDagboek', { timeout: 10000 });
|
||||
// Small delay for async data loading (voedingsmiddelen.json)
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Helper: switch to dagboek tab
|
||||
async function goToDagboek(page) {
|
||||
await page.click('#navDagboek');
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
|
||||
// Helper: open inspiratieSheet via Vind inspiratie button on the first Tussendoor card
|
||||
async function openInspiratieSheet(page) {
|
||||
await goToDagboek(page);
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await expect(inspiratieBtn).toBeVisible();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
// Verify sheet is open
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe('Vind inspiratie feature (bottom sheet)', () => {
|
||||
|
||||
test('Vind inspiratie button only appears on Tussendoor 1, 2, 3 cards in dagboek', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Find all eetmoment cards
|
||||
const cards = await page.locator('.eetmoment-card').all();
|
||||
expect(cards.length).toBeGreaterThanOrEqual(6); // 6 eetmomenten
|
||||
|
||||
for (const card of cards) {
|
||||
const nameEl = card.locator('.eetmoment-name');
|
||||
const name = await nameEl.textContent();
|
||||
const hasInspiratieBtn = await card.locator('.vind-inspiratie-btn').count();
|
||||
|
||||
if (name === 'Tussendoor 1' || name === 'Tussendoor 2' || name === 'Tussendoor 3') {
|
||||
expect(hasInspiratieBtn).toBe(1);
|
||||
} else {
|
||||
expect(hasInspiratieBtn).toBe(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Clicking Vind inspiratie opens bottom sheet with correct context title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Click the first visible "Vind inspiratie" button (Tussendoor 1)
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await expect(inspiratieBtn).toBeVisible();
|
||||
await inspiratieBtn.click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should now have the inspiratieSheet open (bottom sheet)
|
||||
const sheet = page.locator('#inspiratieSheet');
|
||||
await expect(sheet).toBeVisible();
|
||||
await expect(sheet).not.toHaveClass(/hidden/);
|
||||
|
||||
// Sheet title should start with "Inspiratie voor Tussendoor"
|
||||
const title = page.locator('#inspiratieSheetTitle');
|
||||
const titleText = await title.textContent();
|
||||
expect(titleText).toContain('Inspiratie voor Tussendoor');
|
||||
});
|
||||
|
||||
test('Afsluiten X-knop sluit bottom sheet en keert terug naar Dagboek', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Open inspiratie sheet
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet should be open
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
|
||||
// Click X close button
|
||||
await page.click('#inspiratieSheetClose');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet should be hidden
|
||||
await expect(page.locator('#inspiratieSheet')).toHaveClass(/hidden/);
|
||||
|
||||
// Dagboek should still be active
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('Afsluiten via backdrop-click sluit bottom sheet en keert terug naar Dagboek', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Open inspiratie sheet
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet should be open
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
|
||||
// Click the overlay backdrop (not the sheet content)
|
||||
const overlay = page.locator('#inspiratieSheet');
|
||||
// Click outside the modal-sheet area (the overlay itself)
|
||||
await overlay.click({ position: { x: 10, y: 10 } });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet should be hidden
|
||||
await expect(page.locator('#inspiratieSheet')).toHaveClass(/hidden/);
|
||||
|
||||
// Dagboek should still be active
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('Vind inspiratie shows context header with date and moment name in sheet', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Click first Vind inspiratie button
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Context header should be visible in the sheet
|
||||
const contextEl = page.locator('#inspiratieContext');
|
||||
await expect(contextEl).toBeVisible();
|
||||
|
||||
// Context label should contain the moment name
|
||||
const contextLabel = page.locator('#inspiratieContextLabel');
|
||||
const labelText = await contextLabel.textContent();
|
||||
expect(labelText).toContain('Tussendoor');
|
||||
expect(labelText).toContain('202'); // any year prefix in the date
|
||||
});
|
||||
|
||||
test('Context header close button clears inspiration mode but keeps sheet open', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Click Vind inspiratie
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Context is visible
|
||||
await expect(page.locator('#inspiratieContext')).toBeVisible();
|
||||
|
||||
// Click context close (× in the context banner, not the sheet X)
|
||||
await page.click('#inspiratieContextClose');
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
// Title should now show "Tussendoortjes" (inspiratie context cleared)
|
||||
const titleText = await page.locator('#inspiratieSheetTitle').textContent();
|
||||
expect(titleText).toBe('Tussendoortjes');
|
||||
|
||||
// Context should be hidden
|
||||
await expect(page.locator('#inspiratieContext')).toBeHidden();
|
||||
|
||||
// Sheet should still be open (not closed)
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
await expect(page.locator('#inspiratieSheet')).not.toHaveClass(/hidden/);
|
||||
});
|
||||
|
||||
test('Nieuwe suggesties button exists in sheet and generates suggestions', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
// Open sheet via first Vind inspiratie button to get suggestions context
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
// Button should be visible in the sheet
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
await expect(nieuweBtn).toBeVisible();
|
||||
expect(await nieuweBtn.textContent()).toContain('Nieuwe suggesties');
|
||||
|
||||
// Click it - should generate suggestions
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should render suggestions (cards or empty state)
|
||||
const suggestieCards = page.locator('.suggestie-card');
|
||||
const emptyState = page.locator('.inspiratie-empty');
|
||||
const hasCards = (await suggestieCards.count()) > 0;
|
||||
const isEmpty = await emptyState.isVisible();
|
||||
|
||||
// Either we have cards or an empty state (if no suggestions fit the range)
|
||||
expect(hasCards || isEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Nieuwe suggesties changes the set on consecutive presses', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
|
||||
// First press
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
const firstCardNames = await page.locator('.suggestie-card-naam').allTextContents();
|
||||
|
||||
// Second press (only if there are enough ingredients to generate a different set)
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
const secondCardNames = await page.locator('.suggestie-card-naam').allTextContents();
|
||||
|
||||
// If both sets have cards, at least one name should differ
|
||||
if (firstCardNames.length > 0 && secondCardNames.length > 0) {
|
||||
const firstJoined = firstCardNames.join(',');
|
||||
const secondJoined = secondCardNames.join(',');
|
||||
// They should not be exactly identical
|
||||
// (There's a small chance of getting the same set, but it should be rare)
|
||||
try {
|
||||
expect(firstJoined).not.toBe(secondJoined);
|
||||
} catch (e) {
|
||||
// If they're the same, it's possible with a small pool — log a warning
|
||||
console.warn('WARNING: Both presses returned the same set (possible but unlikely)');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('All shown suggestion totals are within the carb range (default 15-20g)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
// Click Nieuwe suggesties a few times to increase coverage
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const totals = await page.locator('.suggestie-card-totaal').allTextContents();
|
||||
for (const t of totals) {
|
||||
// Format: "X.X g kh"
|
||||
const match = t.match(/([\d.]+)/);
|
||||
if (match) {
|
||||
const khValue = parseFloat(match[1]);
|
||||
expect(khValue).toBeGreaterThanOrEqual(15);
|
||||
expect(khValue).toBeLessThanOrEqual(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('All interactive elements have haptic metadata', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Check that Vind inspiratie buttons have data-haptic attribute
|
||||
const inspiratieBtns = page.locator('.vind-inspiratie-btn');
|
||||
const count = await inspiratieBtns.count();
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const haptic = await inspiratieBtns.nth(i).getAttribute('data-haptic');
|
||||
expect(haptic).toBeTruthy();
|
||||
}
|
||||
|
||||
// Open sheet and check Nieuwe suggesties button exists inside
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
await expect(nieuweBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('All shown ingredients have valid NEVO-n in database; kaneel uses actual DB value (56g/100g, not 0)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
|
||||
// Collect data across multiple generations to increase coverage
|
||||
let allIngredients = [];
|
||||
let kaneelFound = false;
|
||||
let kaneelKhPer100 = 0;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Get ingredient data-n and kh from DOM
|
||||
const ingredientData = await page.evaluate(() => {
|
||||
const items = document.querySelectorAll('.suggestie-ingredient');
|
||||
return Array.from(items).map(el => ({
|
||||
n: el.getAttribute('data-n'),
|
||||
khPer100: el.getAttribute('data-kh-per-100'),
|
||||
khText: el.querySelector('.suggestie-ingredient-kh')?.textContent || '',
|
||||
portieText: el.querySelector('.suggestie-ingredient-gram')?.textContent || '',
|
||||
}));
|
||||
});
|
||||
|
||||
allIngredients = allIngredients.concat(ingredientData);
|
||||
|
||||
// Check if kaneel is in this batch — read khPer100 directly from data attribute
|
||||
const kaneelItems = ingredientData.filter(d => d.n === '826');
|
||||
if (kaneelItems.length > 0) {
|
||||
kaneelFound = true;
|
||||
for (const ki of kaneelItems) {
|
||||
const khp = parseFloat(ki.khPer100);
|
||||
if (!isNaN(khp)) {
|
||||
kaneelKhPer100 = khp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify every ingredient has a valid NEVO-n in the database
|
||||
// Load DB from Node.js side (data is not globally exposed due to IIFE)
|
||||
const fs = require('fs');
|
||||
const dbPath = require('path').join(__dirname, '..', 'data', 'voedingsmiddelen.json');
|
||||
let dbNs = new Set();
|
||||
try {
|
||||
const dbData = JSON.parse(fs.readFileSync(dbPath, 'utf-8'));
|
||||
dbNs = new Set(dbData.map(item => String(item.n)));
|
||||
} catch (e) {
|
||||
console.warn('WARNING: Could not load voedingsmiddelen.json, skipping NEVO-n validation:', e.message);
|
||||
}
|
||||
|
||||
if (dbNs.size > 0) {
|
||||
const invalidIngredients = allIngredients.filter(d => d.n && !dbNs.has(d.n));
|
||||
if (invalidIngredients.length > 0) {
|
||||
// Some ingredients couldn't be resolved — fail with details
|
||||
const errMsg = 'All shown ingredients must have a NEVO-n that exists in voedingsmiddelen.json. ' +
|
||||
'Invalid n values: ' + JSON.stringify(invalidIngredients);
|
||||
expect(invalidIngredients, errMsg).toEqual([]);
|
||||
}
|
||||
}
|
||||
|
||||
// If kaneel appeared, verify its kh_per100 rounds to ~56, not 0
|
||||
if (kaneelFound) {
|
||||
const errMsg = 'Kaneel (n=826) should use DB kh value of 56g/100g, not the hardcoded 0 from INSPIRATIE_POOL. ' +
|
||||
'Got ' + kaneelKhPer100 + 'g/100g instead of 56g/100g.';
|
||||
expect(kaneelKhPer100, errMsg).toBe(56);
|
||||
} else {
|
||||
// If kaneel didn't appear by chance, log a warning
|
||||
console.warn('WARNING: Kaneel did not appear in 5 generations — could not verify its DB value');
|
||||
}
|
||||
});
|
||||
|
||||
test('Generated recipe titles use friendly names, not raw NEVO display names', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await openInspiratieSheet(page);
|
||||
|
||||
// Known NEVO display-name fragments that should NEVER appear in a recipe title.
|
||||
// These are raw NEVO aliases that get stripped by displayNaam() — NOT the friendly
|
||||
// names from INSPIRATIE_POOL. "Blauwe bessen" is a valid friendly name, for example.
|
||||
const NEVO_ALIAS_FRAGMENTS = [
|
||||
'z schil gem',
|
||||
'bessen bos-',
|
||||
'Bessen bos-',
|
||||
'z schil',
|
||||
];
|
||||
|
||||
const nieuweBtn = page.locator('#nieuweSuggestiesBtn');
|
||||
let titlesChecked = 0;
|
||||
|
||||
// Generate multiple batches to increase coverage
|
||||
for (let batch = 0; batch < 5; batch++) {
|
||||
await nieuweBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const titles = await page.locator('.suggestie-card-naam').allTextContents();
|
||||
titlesChecked += titles.length;
|
||||
|
||||
for (const title of titles) {
|
||||
const titleLower = title.toLowerCase();
|
||||
for (const fragment of NEVO_ALIAS_FRAGMENTS) {
|
||||
const fragLower = fragment.toLowerCase();
|
||||
expect(
|
||||
titleLower,
|
||||
`Recipe title "${title}" should not contain NEVO alias fragment "${fragment}"`
|
||||
).not.toContain(fragLower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we actually checked some titles (at least one batch had suggestions)
|
||||
if (titlesChecked === 0) {
|
||||
console.warn('WARNING: No suggestion titles were generated across 5 batches — cannot verify friendly names');
|
||||
} else {
|
||||
console.log(`OK: Checked ${titlesChecked} generated titles — no NEVO alias fragments found`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Dagboek blijft actief na openen en sluiten van inspiratie-sheet', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Verify Dagboek is active initially
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
|
||||
// Open sheet
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet is open
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
|
||||
// Dagboek should still be active (no tab switch)
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
|
||||
// Close sheet via X
|
||||
await page.click('#inspiratieSheetClose');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet is hidden
|
||||
await expect(page.locator('#inspiratieSheet')).toHaveClass(/hidden/);
|
||||
|
||||
// Dagboek should still be active
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('Geen navigatie naar tussendoortjes tab (tabTussendoortjes blijft leeg/verborgen)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await waitForApp(page);
|
||||
await goToDagboek(page);
|
||||
|
||||
// Open inspiratie sheet
|
||||
const inspiratieBtn = page.locator('.vind-inspiratie-btn').first();
|
||||
await inspiratieBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Sheet is visible
|
||||
await expect(page.locator('#inspiratieSheet')).toBeVisible();
|
||||
|
||||
// tabTussendoortjes should either be hidden or empty
|
||||
const tussendoortjesTab = page.locator('#tabTussendoortjes');
|
||||
const classAttr = await tussendoortjesTab.getAttribute('class');
|
||||
const isHidden = classAttr && classAttr.includes('hidden');
|
||||
|
||||
if (!isHidden) {
|
||||
// If visible, it should be empty (no suggestieList in it, no content)
|
||||
const suggestieCount = await tussendoortjesTab.locator('.suggestie-card').count();
|
||||
expect(suggestieCount).toBe(0);
|
||||
}
|
||||
|
||||
// Dagboek stays active
|
||||
await expect(page.locator('#navDagboek')).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue