Quality gates / quality (pull_request) Successful in 2m59s
The home insight card now reports the confirmed-log count and form groupings for a window, every line names the exact confirmed records it was built from, and following a citation opens those records. A window below the minimum states that it is too sparse instead of describing a trend, and a window with both firm and loose logs says it is mixed rather than picking a winner. No line explains a cause, rules on food, or offers a health verdict. - src/summary.js: window filtering, insufficient-data guard, per-form lines, mixed-pattern line, boundary copy, record citations - app.js: explainable home card plus a pattern-source sheet listing the cited records - tests/summary.test.js, tests/pattern-summary.acceptance.mjs: unit and real-browser coverage, gated in CI and the release builder - scripts/record_release_demo.mjs: fictional multi-day sample logs, citation follow-through, and the sparse-window refusal on camera
89 lines
4.4 KiB
JavaScript
89 lines
4.4 KiB
JavaScript
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');
|