karby/tests/glucose-e2e.spec.js
cas dbe39fded7 feat: glucose-metingen — nuchter + postprandiaal met streefwaardes
Data-laag: glucose state, localStorage ('koolhydraat-glucose'), PRIKMOMENTEN met
target ranges (nuchter < 5.4, postprandiaal 5.4–6.7)

UI: glucose-sectie in dagboek met 4 inputvelden (nuchter, na ontbijt, na lunch,
na avondeten). Kleurcodering: groen=binnen range, rood=buiten, grijs=leeg.
Save-on-blur, responsive 2×2 grid op mobiel.

E2e: 10 Playwright tests — invoer, validatie, kleurcodering, persistentie,
datum-navigatie, decimale invoer.

35 tests totaal (25 origineel + 10 glucose)
2026-07-24 19:36:40 +02:00

280 lines
10 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const BASE = 'https://eetdagboek.vantwout.dev';
/**
* Helpers for glucose tests
*/
/** Navigate to diary tab and wait for render */
async function openDiary(page) {
await page.locator('#navDagboek').click();
await page.waitForTimeout(600);
// Wait for glucose section to render
await expect(page.locator('.glucose-section')).toBeVisible({ timeout: 5000 });
}
/** Fill a glucose input and trigger save-on-blur */
async function setGlucoseValue(page, momentKey, value) {
const input = page.locator(`input.glucose-input[data-moment="${momentKey}"]`);
await input.fill(String(value));
// Trigger blur — saves to localStorage and re-renders the grid
await input.evaluate(el => el.blur());
// Wait for re-render to complete
await page.waitForTimeout(400);
}
/** Get the parent .glucose-item for a given moment */
function glucoseItem(page, momentKey) {
return page.locator(`input.glucose-input[data-moment="${momentKey}"]`).locator('..').locator('..');
}
/** Get status text element for a given moment */
function statusText(page, momentKey) {
return page.locator(`input.glucose-input[data-moment="${momentKey}"]`)
.locator('..') // .glucose-input-wrapper
.locator('..') // .glucose-item
.locator('.glucose-status-text');
}
// =============================================================================
test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistentie', () => {
// ---- 1. Glucose-sectie zichtbaar ----
test('1. glucose section has 4 input fields with correct labels and target ranges', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
// Section structure
await expect(page.locator('.glucose-section')).toBeVisible();
await expect(page.locator('.glucose-title')).toHaveText('Glucose');
await expect(page.locator('.glucose-unit-label')).toHaveText('mmol/L');
await expect(page.locator('.glucose-header .glucose-icon i')).toBeVisible();
// 4 grid items
const items = page.locator('.glucose-item');
await expect(items).toHaveCount(4);
// Labels
await expect(items.nth(0).locator('.glucose-item-label')).toHaveText('Nuchter');
await expect(items.nth(1).locator('.glucose-item-label')).toHaveText('Ontbijt');
await expect(items.nth(2).locator('.glucose-item-label')).toHaveText('Lunch');
await expect(items.nth(3).locator('.glucose-item-label')).toHaveText('Avondeten');
// Target ranges
await expect(items.nth(0).locator('.glucose-item-range')).toHaveText('< 5.4');
await expect(items.nth(1).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
await expect(items.nth(2).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
await expect(items.nth(3).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
// Input attributes
const moments = ['nuchter', 'naOntbijt', 'naLunch', 'naAvondeten'];
for (let i = 0; i < 4; i++) {
const input = items.nth(i).locator('.glucose-input');
await expect(input).toHaveAttribute('data-moment', moments[i]);
await expect(input).toHaveAttribute('step', '0.1');
await expect(input).toHaveAttribute('min', '0');
await expect(input).toHaveAttribute('max', '30');
await expect(input).toHaveAttribute('inputmode', 'decimal');
await expect(input).toHaveAttribute('placeholder', '\u2014');
}
});
// ---- 2. Nuchter binnen range (groen) ----
test('2. nuchter waarde 5.1 (binnen < 5.4) toont groene indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
await setGlucoseValue(page, 'nuchter', 5.1);
// Check the item class and status
const item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-icon')).toContainText('\u2705'); // ✅
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
});
// ---- 3. Nuchter ≥ 5.4 (rood) ----
test('3. nuchter waarde 5.4 (te hoog, ≥ 5.4) toont rode indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
await setGlucoseValue(page, 'nuchter', 5.4);
const item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-bad/);
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
});
// ---- 4. Postprandiaal < 5.4 (rood/te laag) ----
test('4. naOntbijt waarde 5.3 (te laag, < 5.4) toont rode indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
await setGlucoseValue(page, 'naOntbijt', 5.3);
const item = glucoseItem(page, 'naOntbijt');
await expect(item).toHaveClass(/glucose-bad/);
await expect(item.locator('.glucose-status-text')).toHaveText('Te laag');
});
// ---- 5. Postprandiaal binnen range (groen) ----
test('5. naOntbijt waarde 6.0 (binnen 5.4\u20136.7) toont groene indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
await setGlucoseValue(page, 'naOntbijt', 6.0);
const item = glucoseItem(page, 'naOntbijt');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
});
// ---- 6. Postprandiaal > 6.7 (rood/te hoog) ----
test('6. naLunch waarde 6.8 (te hoog, > 6.7) toont rode indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
await setGlucoseValue(page, 'naLunch', 6.8);
const item = glucoseItem(page, 'naLunch');
await expect(item).toHaveClass(/glucose-bad/);
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
});
// ---- 7. Leeg veld — grijze 'nog meten' indicator ----
test('7. lege velden tonen grijze nog-meten indicator', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
// All 4 items should start empty
const items = page.locator('.glucose-item');
const count = await items.count();
expect(count).toBe(4);
for (let i = 0; i < count; i++) {
await expect(items.nth(i)).toHaveClass(/glucose-empty/);
}
const statusTexts = page.locator('.glucose-status-text');
const texts = await statusTexts.allTextContents();
for (const t of texts) {
expect(t).toBe('Nog meten');
}
// Status icons should be ⏹️
const icons = page.locator('.glucose-status-icon');
const iconTexts = await icons.allTextContents();
for (const icon of iconTexts) {
expect(icon.includes('\u23F9\uFE0F') || icon.includes('\u23F9')).toBeTruthy();
}
// All inputs should have empty value
const inputs = page.locator('.glucose-input');
const inputCount = await inputs.count();
for (let i = 0; i < inputCount; i++) {
await expect(inputs.nth(i)).toHaveValue('');
}
});
// ---- 8. Persistentie — waarde blijft na page reload ----
test('8. persistentie: waarde blijft na page reload bewaard', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
// Set a nuchter value
await setGlucoseValue(page, 'nuchter', 5.1);
// Verify it's shown correctly
let item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
// Reload the page
await page.goto(BASE);
await openDiary(page);
// Value should still be there
item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
});
// ---- 9. Datum-navigatie — andere datum andere waarden ----
test('9. datum-navigatie: andere datum toont lege glucose waarden', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
// Set a nuchter value for today
await setGlucoseValue(page, 'nuchter', 5.1);
// Verify today shows the value
let item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-good/);
// Navigate to tomorrow
await page.locator('#dayNext').click();
await page.waitForTimeout(600);
// Tomorrow should have all empty glucose fields
item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-empty/);
await expect(item.locator('.glucose-status-text')).toHaveText('Nog meten');
await expect(item.locator('.glucose-input')).toHaveValue('');
// Also check the other moments are empty too
for (const moment of ['naOntbijt', 'naLunch', 'naAvondeten']) {
const otherItem = glucoseItem(page, moment);
await expect(otherItem).toHaveClass(/glucose-empty/);
}
// Navigate back to today
await page.locator('#dayToday').click();
await page.waitForTimeout(600);
// Today's value should still be there
item = glucoseItem(page, 'nuchter');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
});
// ---- 10. Decimale invoer — step 0.1 ----
test('10. decimale invoer: step 0.1 accepteert 5.5 en 6.2 correct', async ({ page }) => {
await page.goto(BASE);
await openDiary(page);
// Check step attribute
const input = page.locator('input.glucose-input[data-moment="nuchter"]');
await expect(input).toHaveAttribute('step', '0.1');
// Enter 5.5 (decimal)
await setGlucoseValue(page, 'nuchter', 5.5);
let item = glucoseItem(page, 'nuchter');
await expect(item.locator('.glucose-input')).toHaveValue('5.5');
// 5.5 ≥ 5.4 for nuchter → should be bad/Te hoog
await expect(item).toHaveClass(/glucose-bad/);
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
// Now test a postprandial decimal value
await setGlucoseValue(page, 'naOntbijt', 6.2);
item = glucoseItem(page, 'naOntbijt');
await expect(item.locator('.glucose-input')).toHaveValue('6.2');
await expect(item).toHaveClass(/glucose-good/);
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
});
});