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.
65 lines
3.1 KiB
JavaScript
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');
|