diff --git a/tests/eetmoment-labels.spec.js b/tests/eetmoment-labels.spec.js new file mode 100644 index 0000000..58d7172 --- /dev/null +++ b/tests/eetmoment-labels.spec.js @@ -0,0 +1,123 @@ +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); + }); + }); + } +});