import { chromium } from 'playwright'; import assert from 'node:assert/strict'; import { mkdir } from 'node:fs/promises'; // Issue #34: every longitudinal summary line must point at the confirmed records behind it, and a // sparse window must say so instead of describing a trend. The records are fictional sample entries // written into the app's own local ledger contract; no real health data is involved. const DAY = 86400000; const sample = (id, daysAgo, bristolType) => ({ id, occurredAt: new Date(Date.now() - daysAgo * DAY).toISOString(), bristolType, color: 'brown', urgency: 1, discomfort: 1, note: '', photoDataUrl: '', symptoms: {}, }); await mkdir('artifacts', { recursive: true }); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, serviceWorkers: 'block' }); const page = await context.newPage(); const errors = []; page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); page.on('pageerror', error => errors.push(error.message)); const seed = async entries => page.evaluate(list => { const base = document.querySelector('meta[name="timmy-base-path"]')?.content || '/'; localStorage.setItem(`timmy:${base}:ledger-v1`, JSON.stringify(list)); }, entries); await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' }); await page.evaluate(() => localStorage.clear()); // 1. A sparse window refuses to describe a pattern. await seed([sample('sparse-a', 1, 4), sample('sparse-b', 4, 6)]); await page.reload({ waitUntil: 'networkidle' }); const sparse = page.locator('[data-summary-insufficient]'); assert.equal(await sparse.count(), 1, 'a window below the minimum must show honest insufficient-data copy'); assert.match(await sparse.innerText(), /not enough confirmed logs/i); assert.match(await sparse.innerText(), /2 of 3/); assert.equal(await page.locator('[data-statement]').count(), 0, 'no pattern line may be claimed from two logs'); await page.screenshot({ path: 'artifacts/pattern-summary-insufficient-mobile.png' }); // 2. A populated window renders explainable lines, each citing its own records. const seeded = [ sample('rec-firm', 2, 1), sample('rec-typical-1', 5, 3), sample('rec-typical-2', 9, 4), sample('rec-typical-3', 12, 4), sample('rec-old', 40, 6), ]; await seed(seeded); await page.reload({ waitUntil: 'networkidle' }); const card = page.locator('[data-pattern-summary]'); assert.equal(await card.count(), 1, 'the home insight card is the explainable summary'); assert.match(await page.locator('[data-summary-window]').innerText(), /last 28 days/i); assert.match(await page.locator('[data-statement="frequency"]').innerText(), /4 confirmed logs/); assert.equal(await page.locator('[data-statement="firm"]').getAttribute('data-records'), '1'); assert.equal(await page.locator('[data-statement="typical"]').getAttribute('data-records'), '3'); // The out-of-window log is neither counted nor offered as a citation. assert.doesNotMatch(await card.innerText(), /5 confirmed logs/); await page.screenshot({ path: 'artifacts/pattern-summary-home-mobile.png' }); // 3. Following a citation shows exactly the contributing confirmed records. await page.locator('[data-statement-link="typical"]').click(); const records = page.locator('[data-summary-records]'); await records.waitFor(); assert.equal(await page.locator('[data-summary-record]').count(), 3, 'the citation lists exactly its own records'); const listed = await page.locator('[data-summary-record]').evaluateAll(nodes => nodes.map(node => node.getAttribute('data-entry-id'))); assert.deepEqual(listed.sort(), ['rec-typical-1', 'rec-typical-2', 'rec-typical-3']); assert.match(await records.innerText(), /Type 3/); assert.match(await records.innerText(), /These are the confirmed logs this line was built from/); await page.screenshot({ path: 'artifacts/pattern-summary-records-mobile.png' }); await page.locator('#pattern-records-close').click(); assert.equal(await page.locator('[data-summary-records]').count(), 0, 'the citation sheet closes cleanly'); // 4. The summary never explains a cause or clears a food. const summaryText = await card.innerText(); assert.doesNotMatch(summaryText, /because|due to|caused|safe to eat|avoid eating|should eat|diagnos/i); assert.deepEqual(errors, []); await context.close(); await browser.close(); console.log('PASS longitudinal pattern summaries cite their confirmed records and refuse to describe a sparse window');