All checks were successful
Quality gates / quality (pull_request) Successful in 4m14s
- collision-safe ID repair: duplicate ids inside stored data are repaired deterministically (first keeps id, twins get id#2, id#3, ... scanning past owned suffixes); every distinct local record survives, never dropped or silently merged; hostile id types (Symbol/BigInt/objects) repair onto fresh deterministic ids instead of throwing - transactional import: parse+merge into a candidate ledger, persist first, then commit memory; quota/error rolls back in-memory state and localStorage together with explicit user feedback; total 16MiB portability budget enforced before mutation on export, import (post-migration expansion), and storage writes - strict Timmy legacy contract for bare top-level arrays: nonempty array of plain rows each carrying a nonempty string id and integer Bristol 1-7; arbitrary unrelated arrays are rejected wholesale - no invented medical defaults from foreign JSON - canonical raster photo validation: strict JPEG/PNG/WebP grammar, canonical base64 (linear scan, no regex on multi-MB strings), atob round-trip decode, declared-format magic bytes, 32B-4MiB decoded bounds; mislabeled SVG/HTML and noncanonical tiny junk are stripped while genuine photos survive byte-for-byte - migrateStoredLedger: localStorage is validated and migrated before render; invalid dates become safe ISO timestamps, duplicate ids repaired, junk rows dropped (never fabricated into default records); healthy storage is byte-stable and never rewritten - sanitizeEntry absorbs Symbol/BigInt/hostile dates/throwing toString, valueOf, getTime, toJSON without throwing; results stay serializable - browser regression suite: quota rollback, pre-render migration, array rejection, photo contract, duplicate-ID preservation in the real app flow - staging-health startup-rejection budget anchored to measured server cold-start instead of a fixed 800ms (fixes load-sensitive flake)
61 lines
3.4 KiB
JavaScript
61 lines
3.4 KiB
JavaScript
import { chromium } from 'playwright';
|
||
import assert from 'node:assert/strict';
|
||
import { mkdir } from 'node:fs/promises';
|
||
|
||
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));
|
||
await page.route('**/api/vision-status', route => route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({ enabled: true, profile: 'selfhost', processor: 'self-hosted', model: 'SmolVLM2-2.2B-Instruct', providerReady: true, modelSeen: true }),
|
||
}));
|
||
await page.route('**/api/analyze', route => route.fulfill({
|
||
status: 200,
|
||
contentType: 'application/json',
|
||
body: JSON.stringify({
|
||
status: 'suggestion', isStool: true, bristolType: 4, color: 'brown', confidence: 0.83,
|
||
imageQuality: 'good', observations: 'Smooth, formed appearance.',
|
||
warning: 'Visual suggestion only. Confirm it yourself; this is not a diagnosis.',
|
||
}),
|
||
}));
|
||
const appUrl = process.env.TIMMY_TEST_URL || 'http://127.0.0.1:4173';
|
||
await page.goto(appUrl, { waitUntil: 'networkidle' });
|
||
await page.evaluate(() => localStorage.clear());
|
||
await page.reload({ waitUntil: 'networkidle' });
|
||
|
||
await page.locator('[data-scan]').click();
|
||
await page.getByText(/Self-hosted model ready/i).waitFor();
|
||
await page.screenshot({ path: 'artifacts/selfhost-photo-first-mobile.png', fullPage: false });
|
||
assert.equal(await page.getByText('One photo. Two useful suggestions.').isVisible(), true);
|
||
await page.locator('#ai-photo').setInputFiles('tests/fixtures/synthetic-type4.jpg');
|
||
assert.equal(await page.locator('#analyze-photo').isDisabled(), true);
|
||
assert.match(await page.locator('.consent-card').innerText(), /self-hosted model server/i);
|
||
assert.doesNotMatch(await page.locator('.consent-card').innerText(), /provider’s terms/i);
|
||
await page.locator('#ai-consent').check();
|
||
assert.equal(await page.locator('#analyze-photo').isEnabled(), true);
|
||
await page.locator('#analyze-photo').click();
|
||
await page.getByText(/83% confidence/i).waitFor();
|
||
assert.equal(await page.getByText('Type 4', { exact: true }).isVisible(), true);
|
||
assert.equal(await page.getByText('brown', { exact: true }).isVisible(), true);
|
||
assert.match(await page.locator('.scan-result .fine').innerText(), /not a diagnosis/i);
|
||
await page.screenshot({ path: 'artifacts/photo-first-result-mobile.png', fullPage: false });
|
||
|
||
await page.locator('#use-suggestion').click();
|
||
assert.equal(await page.getByText('AI PREFILLED', { exact: true }).isVisible(), true);
|
||
assert.equal(await page.locator('[data-type="4"]').getAttribute('class').then(v => v.includes('selected')), true);
|
||
await page.locator('#next').click();
|
||
assert.equal(await page.locator('#color').inputValue(), 'brown');
|
||
assert.equal(await page.locator('#urgency').inputValue(), '0');
|
||
assert.equal(await page.locator('#discomfort').inputValue(), '0');
|
||
assert.match(await page.locator('.fine').last().innerText(), /must come from you/i);
|
||
await page.screenshot({ path: 'artifacts/photo-first-prefill-mobile.png', fullPage: false });
|
||
|
||
assert.deepEqual(errors, []);
|
||
await browser.close();
|
||
console.log('PASS photo → consent → AI suggestion → confirmed visual prefill → nonvisual fields remain user-reported');
|