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.', }), })); await page.goto('http://127.0.0.1:4173', { 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('#gallery-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');