From 85ef8ba2defe453ddb2f50a32eb06d438c3edd3b Mon Sep 17 00:00:00 2001 From: cas Date: Tue, 28 Jul 2026 20:39:52 +0200 Subject: [PATCH] =?UTF-8?q?UX=20=E2=80=94=20open=20tussendoortjessuggestie?= =?UTF-8?q?s=20als=20bottom=20sheet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Verplaatst de bestaande tabTussendoortjes-inhoud naar een nieuwe inspiratieSheet bottom-sheet overlay (reuseert .modal-overlay, .modal-sheet, .modal-handle, backdrop-close en setupSwipeDown). - Klikken op 'Vind inspiratie' bij Tussendoor 1/2/3 opent nu de bottom sheet i.p.v. volledige tab-navigatie. - Sheet heeft X-knop, backdrop-click en swipe-down om te sluiten. - Haptics bij openen (10ms) en sluiten (6ms). - renderTussendoortjes() werkt zowel in tab als sheet context. - clearInspiratieContext() herstelt context ook als sheet open is. - Playwright-tests geüpdatet: verifiëren sheet open/sluit, titel, context, X/backdrop close, Dagboek blijft actief, geen tab-navigatie. - docker/index.html gesynchroniseerd. --- docker/index.html | 166 +++++++++++++++++++++++++++---------- index.html | 166 +++++++++++++++++++++++++++---------- tests/e2e.spec.js | 132 +++++++++++++++++------------ tests/inspiratie.spec.js | 175 +++++++++++++++++++++++++++++++++------ 4 files changed, 467 insertions(+), 172 deletions(-) diff --git a/docker/index.html b/docker/index.html index 99aadf3..8805f99 100644 --- a/docker/index.html +++ b/docker/index.html @@ -2840,46 +2840,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- - + @@ -2946,6 +2908,55 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- + + +
@@ -6022,21 +6033,27 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } function renderTussendoortjes() { - if (activeTab !== 'tussendoortjes') return; + if (activeTab !== 'tussendoortjes') { + var sheet = document.getElementById('inspiratieSheet'); + var isSheetOpen = sheet && !sheet.classList.contains('hidden') && !sheet.classList.contains('closing'); + if (!isSheetOpen) return; + } const listEl = document.getElementById('suggestieList'); if (!listEl) return; const titleEl = document.getElementById('tussendoortjesTitle'); + const sheetTitleEl = document.getElementById('inspiratieSheetTitle'); const contextEl = document.getElementById('inspiratieContext'); const contextLabel = document.getElementById('inspiratieContextLabel'); // Update title and context based on inspiration mode - if (_inspiratieMoment && titleEl) { - titleEl.textContent = 'Inspiratie voor ' + _inspiratieMoment; - } else { - titleEl.textContent = 'Tussendoortjes'; + var titleText = 'Tussendoortjes'; + if (_inspiratieMoment) { + titleText = 'Inspiratie voor ' + _inspiratieMoment; } + if (titleEl) titleEl.textContent = titleText; + if (sheetTitleEl) sheetTitleEl.textContent = titleText; // Show/hide context header if (contextEl) { @@ -6129,7 +6146,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- _inspiratieMoment = momentId; _inspiratieDate = dateStr; _cachedGeneratedSet = null; - switchTab('tussendoortjes'); + openInspiratieSheet(); } /** @@ -6139,9 +6156,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- _inspiratieMoment = null; _inspiratieDate = null; _cachedGeneratedSet = null; - // Re-render if on tussendoortjes tab + // Re-render if on tussendoortjes tab or if sheet is open if (activeTab === 'tussendoortjes') { renderTussendoortjes(); + } else { + var sheet = document.getElementById('inspiratieSheet'); + var isSheetOpen = sheet && !sheet.classList.contains('hidden') && !sheet.classList.contains('closing'); + if (isSheetOpen) { + renderTussendoortjes(); + } } } @@ -6266,6 +6289,44 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, { passive: true }); } + /** + * Open the inspiratie bottom sheet and render suggestions. + */ + function openInspiratieSheet() { + var overlay = document.getElementById('inspiratieSheet'); + if (!overlay) return; + overlay.classList.remove('hidden', 'closing'); + document.body.style.overflow = 'hidden'; + + // Render tussendoortjes content in the sheet + renderTussendoortjes(); + + // Haptic feedback + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(10); + } + } + + /** + * Close the inspiratie bottom sheet and return to dagboek context. + */ + function closeInspiratieSheet() { + var overlay = document.getElementById('inspiratieSheet'); + if (!overlay) return; + overlay.classList.add('closing'); + setTimeout(function() { + overlay.classList.add('hidden'); + overlay.classList.remove('closing'); + document.body.style.overflow = ''; + clearInspiratieContext(); + }, 280); + + // Haptic feedback + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(6); + } + } + function showToast(message) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); @@ -6844,6 +6905,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.getElementById('detailModalClose').addEventListener('click', () => closeModal('detailModal')); document.getElementById('addMealModalClose').addEventListener('click', () => closeModal('addMealModal')); + // Inspiratie sheet close + var inspiratieSheetClose = document.getElementById('inspiratieSheetClose'); + if (inspiratieSheetClose) inspiratieSheetClose.addEventListener('click', closeInspiratieSheet); + // Close modals on overlay click document.getElementById('detailModal').addEventListener('click', (e) => { if (e.target === document.getElementById('detailModal')) closeModal('detailModal'); @@ -6852,6 +6917,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('addMealModal')) closeModal('addMealModal'); }); + // Inspiratie sheet backdrop close + swipe-down + var inspiratieSheet = document.getElementById('inspiratieSheet'); + if (inspiratieSheet) { + inspiratieSheet.addEventListener('click', function(e) { + if (e.target === inspiratieSheet) closeInspiratieSheet(); + }); + setupSwipeDown(inspiratieSheet, closeInspiratieSheet); + } + // Swipe-down to close for static modals setupSwipeDown(document.getElementById('detailModal'), () => closeModal('detailModal')); setupSwipeDown(document.getElementById('addMealModal'), () => closeModal('addMealModal')); diff --git a/index.html b/index.html index 99aadf3..8805f99 100644 --- a/index.html +++ b/index.html @@ -2840,46 +2840,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- - + @@ -2946,6 +2908,55 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- + + +
@@ -6022,21 +6033,27 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } function renderTussendoortjes() { - if (activeTab !== 'tussendoortjes') return; + if (activeTab !== 'tussendoortjes') { + var sheet = document.getElementById('inspiratieSheet'); + var isSheetOpen = sheet && !sheet.classList.contains('hidden') && !sheet.classList.contains('closing'); + if (!isSheetOpen) return; + } const listEl = document.getElementById('suggestieList'); if (!listEl) return; const titleEl = document.getElementById('tussendoortjesTitle'); + const sheetTitleEl = document.getElementById('inspiratieSheetTitle'); const contextEl = document.getElementById('inspiratieContext'); const contextLabel = document.getElementById('inspiratieContextLabel'); // Update title and context based on inspiration mode - if (_inspiratieMoment && titleEl) { - titleEl.textContent = 'Inspiratie voor ' + _inspiratieMoment; - } else { - titleEl.textContent = 'Tussendoortjes'; + var titleText = 'Tussendoortjes'; + if (_inspiratieMoment) { + titleText = 'Inspiratie voor ' + _inspiratieMoment; } + if (titleEl) titleEl.textContent = titleText; + if (sheetTitleEl) sheetTitleEl.textContent = titleText; // Show/hide context header if (contextEl) { @@ -6129,7 +6146,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- _inspiratieMoment = momentId; _inspiratieDate = dateStr; _cachedGeneratedSet = null; - switchTab('tussendoortjes'); + openInspiratieSheet(); } /** @@ -6139,9 +6156,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- _inspiratieMoment = null; _inspiratieDate = null; _cachedGeneratedSet = null; - // Re-render if on tussendoortjes tab + // Re-render if on tussendoortjes tab or if sheet is open if (activeTab === 'tussendoortjes') { renderTussendoortjes(); + } else { + var sheet = document.getElementById('inspiratieSheet'); + var isSheetOpen = sheet && !sheet.classList.contains('hidden') && !sheet.classList.contains('closing'); + if (isSheetOpen) { + renderTussendoortjes(); + } } } @@ -6266,6 +6289,44 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, { passive: true }); } + /** + * Open the inspiratie bottom sheet and render suggestions. + */ + function openInspiratieSheet() { + var overlay = document.getElementById('inspiratieSheet'); + if (!overlay) return; + overlay.classList.remove('hidden', 'closing'); + document.body.style.overflow = 'hidden'; + + // Render tussendoortjes content in the sheet + renderTussendoortjes(); + + // Haptic feedback + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(10); + } + } + + /** + * Close the inspiratie bottom sheet and return to dagboek context. + */ + function closeInspiratieSheet() { + var overlay = document.getElementById('inspiratieSheet'); + if (!overlay) return; + overlay.classList.add('closing'); + setTimeout(function() { + overlay.classList.add('hidden'); + overlay.classList.remove('closing'); + document.body.style.overflow = ''; + clearInspiratieContext(); + }, 280); + + // Haptic feedback + if (typeof navigator !== 'undefined' && navigator.vibrate) { + navigator.vibrate(6); + } + } + function showToast(message) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); @@ -6844,6 +6905,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.getElementById('detailModalClose').addEventListener('click', () => closeModal('detailModal')); document.getElementById('addMealModalClose').addEventListener('click', () => closeModal('addMealModal')); + // Inspiratie sheet close + var inspiratieSheetClose = document.getElementById('inspiratieSheetClose'); + if (inspiratieSheetClose) inspiratieSheetClose.addEventListener('click', closeInspiratieSheet); + // Close modals on overlay click document.getElementById('detailModal').addEventListener('click', (e) => { if (e.target === document.getElementById('detailModal')) closeModal('detailModal'); @@ -6852,6 +6917,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (e.target === document.getElementById('addMealModal')) closeModal('addMealModal'); }); + // Inspiratie sheet backdrop close + swipe-down + var inspiratieSheet = document.getElementById('inspiratieSheet'); + if (inspiratieSheet) { + inspiratieSheet.addEventListener('click', function(e) { + if (e.target === inspiratieSheet) closeInspiratieSheet(); + }); + setupSwipeDown(inspiratieSheet, closeInspiratieSheet); + } + // Swipe-down to close for static modals setupSwipeDown(document.getElementById('detailModal'), () => closeModal('detailModal')); setupSwipeDown(document.getElementById('addMealModal'), () => closeModal('addMealModal')); diff --git a/tests/e2e.spec.js b/tests/e2e.spec.js index 7d62002..e2787cf 100644 --- a/tests/e2e.spec.js +++ b/tests/e2e.spec.js @@ -578,29 +578,41 @@ test.describe('Karby Eetdagboek', () => { // ========== TUSSENDOORTJES (via inspiratie-flow, nav-knop verwijderd) ========== - test('tussendoortjes tab: bottom-nav button is removed, tab accessible via switchTab', async ({ page }) => { + 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(); - // 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(); + // 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 tab: default range shows suggestie cards', async ({ page }) => { + test('tussendoortjes inspiratie: default range shows suggestie cards in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); - await page.evaluate(() => switchTab('tussendoortjes')); + 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('.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 @@ -611,21 +623,23 @@ test.describe('Karby Eetdagboek', () => { await expect(firstCard.locator('.suggestie-bereiding')).toBeVisible(); }); - test('tussendoortjes tab: filter by range shows only matching totals', async ({ page }) => { + test('tussendoortjes inspiratie: filter by range in sheet shows only matching totals', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); - await page.evaluate(() => switchTab('tussendoortjes')); + 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('#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.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('.suggestie-card-totaal'); + 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(); @@ -635,46 +649,52 @@ test.describe('Karby Eetdagboek', () => { } }); - test('tussendoortjes tab: narrow range shows empty state', async ({ page }) => { + test('tussendoortjes inspiratie: narrow range shows empty state in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); - await page.evaluate(() => switchTab('tussendoortjes')); + 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('#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.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('.suggestie-card'); + const cards = page.locator('#inspiratieSheet .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(); + // Empty state text should be visible in the sheet + await expect(page.locator('#inspiratieSheet .inspiratie-empty')).toBeVisible(); }); - test('tussendoortjes tab: warning label is present', async ({ page }) => { + test('tussendoortjes inspiratie: warning label is present in sheet', 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 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 tab: each ingredient shows name, grams, and kh', async ({ page }) => { + test('tussendoortjes inspiratie: each ingredient shows name, grams, and kh in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(2000); - await page.evaluate(() => switchTab('tussendoortjes')); + await page.click('#navDagboek'); + await page.waitForTimeout(600); + await page.locator('.vind-inspiratie-btn').first().click(); await page.waitForTimeout(1000); - const ingredients = page.locator('.suggestie-ingredient'); + 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 @@ -689,19 +709,21 @@ test.describe('Karby Eetdagboek', () => { 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 }) => { + test('tussendoortjes inspiratie: range value 0 is not defaulted to 15 in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1500); - await page.evaluate(() => switchTab('tussendoortjes')); + 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('#rangeMin').click({ clickCount: 3 }); - await page.locator('#rangeMin').fill('0'); + 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('#rangeMin')).toHaveValue('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(() => { @@ -717,17 +739,19 @@ test.describe('Karby Eetdagboek', () => { expect(parseResult.newFallback).toBe(0); // isNaN(0) ? 15 : 0 → 0 (fixed) }); - test('tussendoortjes tab: range 0–1 is interpreted correctly and shows empty state', async ({ page }) => { + 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.evaluate(() => switchTab('tussendoortjes')); + 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('#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.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 @@ -743,21 +767,23 @@ test.describe('Karby Eetdagboek', () => { expect(rangeValues.rangeMax).toBe(1); // Should show empty state - const cards = page.locator('.suggestie-card'); + const cards = page.locator('#inspiratieSheet .suggestie-card'); const cardCount = await cards.count(); expect(cardCount).toBe(0); - await expect(page.locator('#tabTussendoortjes .empty-state-text')).toBeVisible(); + await expect(page.locator('#inspiratieSheet .inspiratie-empty')).toBeVisible(); }); - test('tussendoortjes tab: range inputs have haptic metadata', async ({ page }) => { + test('tussendoortjes inspiratie: range inputs have haptic metadata in sheet', async ({ page }) => { await page.goto('/'); await page.waitForTimeout(1000); - await page.evaluate(() => switchTab('tussendoortjes')); + 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('#rangeMin')).toHaveAttribute('data-haptic', 'normal'); - await expect(page.locator('#rangeMax')).toHaveAttribute('data-haptic', 'normal'); + await expect(page.locator('#inspiratieSheet #rangeMin')).toHaveAttribute('data-haptic', 'normal'); + await expect(page.locator('#inspiratieSheet #rangeMax')).toHaveAttribute('data-haptic', 'normal'); }); }); diff --git a/tests/inspiratie.spec.js b/tests/inspiratie.spec.js index 3c4fafb..56645cd 100644 --- a/tests/inspiratie.spec.js +++ b/tests/inspiratie.spec.js @@ -3,6 +3,8 @@ 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 @@ -25,13 +27,18 @@ async function goToDagboek(page) { await page.waitForTimeout(400); } -// Helper: switch to tussendoortjes tab (via switchTab, nav-knop is verwijderd) -async function goToTussendoortjes(page) { - await page.evaluate(() => switchTab('tussendoortjes')); - 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', () => { +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('/'); @@ -55,7 +62,7 @@ test.describe('Vind inspiratie feature', () => { } }); - test('Clicking Vind inspiratie opens tussendoortjes with correct context title', async ({ page }) => { + test('Clicking Vind inspiratie opens bottom sheet with correct context title', async ({ page }) => { await page.goto('/'); await waitForApp(page); await goToDagboek(page); @@ -67,16 +74,68 @@ test.describe('Vind inspiratie feature', () => { await page.waitForTimeout(500); - // Should now be on tussendoortjes tab - const title = page.locator('#tussendoortjesTitle'); - await expect(title).toBeVisible(); + // Should now have the inspiratieSheet open (bottom sheet) + const sheet = page.locator('#inspiratieSheet'); + await expect(sheet).toBeVisible(); + await expect(sheet).not.toHaveClass(/hidden/); - // Title should start with "Inspiratie voor Tussendoor" + // 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('Vind inspiratie shows context header with date and moment name', async ({ page }) => { + 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); @@ -86,7 +145,7 @@ test.describe('Vind inspiratie feature', () => { await inspiratieBtn.click(); await page.waitForTimeout(500); - // Context header should be visible + // Context header should be visible in the sheet const contextEl = page.locator('#inspiratieContext'); await expect(contextEl).toBeVisible(); @@ -97,7 +156,7 @@ test.describe('Vind inspiratie feature', () => { expect(labelText).toContain('202'); // any year prefix in the date }); - test('Context header close button clears inspiration mode and shows normal title', async ({ page }) => { + test('Context header close button clears inspiration mode but keeps sheet open', async ({ page }) => { await page.goto('/'); await waitForApp(page); await goToDagboek(page); @@ -110,24 +169,29 @@ test.describe('Vind inspiratie feature', () => { // Context is visible await expect(page.locator('#inspiratieContext')).toBeVisible(); - // Click close + // Click context close (× in the context banner, not the sheet X) await page.click('#inspiratieContextClose'); await page.waitForTimeout(400); - // Title should now be "Tussendoortjes" - const titleText = await page.locator('#tussendoortjesTitle').textContent(); + // 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 and generates suggestions', async ({ page }) => { + test('Nieuwe suggesties button exists in sheet and generates suggestions', async ({ page }) => { await page.goto('/'); await waitForApp(page); - await goToTussendoortjes(page); + // Open sheet via first Vind inspiratie button to get suggestions context + await openInspiratieSheet(page); - // Button should be visible + // Button should be visible in the sheet const nieuweBtn = page.locator('#nieuweSuggestiesBtn'); await expect(nieuweBtn).toBeVisible(); expect(await nieuweBtn.textContent()).toContain('Nieuwe suggesties'); @@ -149,7 +213,7 @@ test.describe('Vind inspiratie feature', () => { test('Nieuwe suggesties changes the set on consecutive presses', async ({ page }) => { await page.goto('/'); await waitForApp(page); - await goToTussendoortjes(page); + await openInspiratieSheet(page); const nieuweBtn = page.locator('#nieuweSuggestiesBtn'); @@ -181,7 +245,7 @@ test.describe('Vind inspiratie feature', () => { test('All shown suggestion totals are within the carb range (default 15-20g)', async ({ page }) => { await page.goto('/'); await waitForApp(page); - await goToTussendoortjes(page); + await openInspiratieSheet(page); // Click Nieuwe suggesties a few times to increase coverage const nieuweBtn = page.locator('#nieuweSuggestiesBtn'); @@ -216,11 +280,9 @@ test.describe('Vind inspiratie feature', () => { expect(haptic).toBeTruthy(); } - await goToTussendoortjes(page); + // Open sheet and check Nieuwe suggesties button exists inside + await openInspiratieSheet(page); - // Check Nieuwe suggesties button has haptic - // (data-haptic is set globally via the event delegation on buttons, - // but the button itself should trigger haptic via the global setup) const nieuweBtn = page.locator('#nieuweSuggestiesBtn'); await expect(nieuweBtn).toBeVisible(); }); @@ -228,7 +290,7 @@ test.describe('Vind inspiratie feature', () => { 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 goToTussendoortjes(page); + await openInspiratieSheet(page); const nieuweBtn = page.locator('#nieuweSuggestiesBtn'); @@ -303,7 +365,7 @@ test.describe('Vind inspiratie feature', () => { test('Generated recipe titles use friendly names, not raw NEVO display names', async ({ page }) => { await page.goto('/'); await waitForApp(page); - await goToTussendoortjes(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 @@ -345,4 +407,63 @@ test.describe('Vind inspiratie feature', () => { 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/); + }); + });