Files
timmy-talking-turd/tests/mobile-capture.acceptance.mjs
T
timmy 9b72872466 feat(photo): give an unreadable file its own upload-free state instead of a failed analysis (#16)
Selecting a file the browser cannot decode used to render "Timmy couldn't analyze that safely",
which described an upload that never happened. It now states that the file isn't a photo Timmy can
read and that nothing was uploaded, with manual entry and the picker both one tap away. The state is
asserted in a new browser suite that also asks the running server to refuse the same bytes, and the
existing mobile capture suite asserts the new copy across both viewports.
2026-10-20 08:21:24 +00:00

65 lines
3.1 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
const browser = await chromium.launch({ headless: true });
const viewports = [
{ name: '390x844', width: 390, height: 844 },
{ name: 'iPhone 15 class', width: 393, height: 852 },
];
for (const viewport of viewports) {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: 2,
serviceWorkers: 'block',
});
const page = await context.newPage();
let analysisRequests = 0;
await page.route('**/api/vision-status', route => route.fulfill({
status: 200,
contentType: 'application/json',
// The fixture mirrors the real /api/vision-status contract, which reports whether the pinned
// model was actually seen loaded (#15) alongside provider reachability.
body: JSON.stringify({ enabled: true, profile: 'selfhost', providerReady: true, modelSeen: true, model: 'synthetic-test-model' }),
}));
await page.route('**/api/analyze', route => {
analysisRequests += 1;
return route.abort();
});
await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' });
await page.locator('[data-scan]').click();
const camera = page.locator('#camera-photo');
const gallery = page.locator('#gallery-photo');
assert.equal(await camera.getAttribute('capture'), 'environment', `${viewport.name}: camera input uses the rear camera`);
assert.equal(await gallery.getAttribute('capture'), null, `${viewport.name}: gallery input does not force camera capture`);
assert.equal(await page.getByText('Take photo', { exact: true }).isVisible(), true);
assert.equal(await page.getByText('Choose from gallery', { exact: true }).isVisible(), true);
await camera.dispatchEvent('cancel');
assert.match(await page.locator('[role="status"]').innerText(), /camera.*closed|permission.*denied/i);
assert.equal(await page.getByText('Continue without AI', { exact: true }).isVisible(), true);
assert.equal(analysisRequests, 0, `${viewport.name}: cancellation never uploads`);
await page.locator('#gallery-photo').setInputFiles({
name: 'corrupt-synthetic.jpg',
mimeType: 'image/jpeg',
buffer: Buffer.from('not an image'),
});
assert.match(await page.locator('.scan-result').innerText(), /isn.t a photo Timmy can read/i);
// Issue #16: the refusal is its own state and states plainly that nothing left the device, instead
// of borrowing the "couldn't analyze that safely" copy from a failed upload.
assert.match(await page.locator('.scan-result').innerText(), /Nothing was uploaded/i);
await page.getByText('Choose another photo', { exact: true }).click();
await page.locator('#gallery-photo').setInputFiles('tests/fixtures/synthetic-type4.jpg');
assert.equal(await page.locator('#analyze-photo').isDisabled(), true);
assert.equal(await page.locator('#retake-photo').isVisible(), true);
assert.equal(analysisRequests, 0, `${viewport.name}: corrupt and unconsented photos never upload`);
await context.close();
}
await browser.close();
console.log('PASS camera/gallery paths recover from cancellation at 390x844 and iPhone-class viewport without upload');