20a7d7d9d5
Locked UI/copy pass on proto-per-user-woo-creds (Settings/Woo gate polish out of scope): - Inventory PNG thumbs at 3:4 contain (Review, Publish preview, Photos ready); stop 1:1 cover squaring - Dimension overlay labels fixed OVERLAY_FONT_PX=36 for every product - Page 1–3 Jeff-plain copy; drop Measure red tech essays / px scale rows - Review: lock SVG, consistent red missing/not-ready, View prompt gated (Settings default off)
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import fs from 'node:fs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const FIXTURES = path.resolve(__dirname, '../fixtures/set1-48x2');
|
|
const ARTIFACTS = path.resolve(__dirname, 'artifacts');
|
|
const FACE_A = path.join(FIXTURES, 'face-a.jpeg');
|
|
const FACE_B = path.join(FIXTURES, 'face-b.jpeg');
|
|
|
|
const shot = async (page: Page, name: string) => {
|
|
fs.mkdirSync(ARTIFACTS, { recursive: true });
|
|
await page.screenshot({
|
|
path: path.join(ARTIFACTS, name),
|
|
fullPage: true,
|
|
});
|
|
};
|
|
|
|
/** Discard any device draft so we always start on Batch. */
|
|
const ensureFreshBatch = async (page: Page) => {
|
|
await page.goto('/');
|
|
// resumeProbe is async onMount — wait for either Resume or Batch.
|
|
const resumeHeading = page.getByRole('heading', { name: /resume/i });
|
|
const batchHeading = page.getByRole('heading', { name: 'Add slab photos' });
|
|
await expect(resumeHeading.or(batchHeading)).toBeVisible({ timeout: 15_000 });
|
|
if (await resumeHeading.isVisible().catch(() => false)) {
|
|
await page.getByRole('button', { name: /start fresh/i }).click();
|
|
}
|
|
await expect(batchHeading).toBeVisible();
|
|
};
|
|
|
|
/**
|
|
* Black-sheet fixtures: Auto may work, but UAT note says prefer Black + sensitivity
|
|
* tweaks. Do not invent inference — only UI sheet/sensitivity controls.
|
|
*/
|
|
const prepareMaskForApprove = async (page: Page) => {
|
|
await expect(page.getByRole('heading', { name: 'Mask' })).toBeVisible();
|
|
// Sheet change recomputes immediately (session.setSheet → runPass).
|
|
await page.getByRole('button', { name: 'Black', exact: true }).click();
|
|
|
|
const approve = page.getByRole('button', { name: 'Approve mask' });
|
|
// First overlay after decode is debounced 2s; Black pass is immediate but
|
|
// 4K fixtures still need decode + worker time.
|
|
try {
|
|
await expect(approve).toBeEnabled({ timeout: 45_000 });
|
|
return;
|
|
} catch {
|
|
/* try sensitivity ladder */
|
|
}
|
|
|
|
// Higher sensitivity widens black sheet match; if still empty, try lower.
|
|
for (const target of [60, 70, 40, 30, 80]) {
|
|
const slider = page.locator('input[type="range"]');
|
|
await slider.fill(String(target));
|
|
// Sensitivity is debounced 2s — wait past debounce + worker.
|
|
await page.waitForTimeout(2800);
|
|
if (await approve.isEnabled()) return;
|
|
}
|
|
|
|
await expect(approve, 'Mask approve stayed disabled after Black + sensitivity tweaks').toBeEnabled({
|
|
timeout: 15_000,
|
|
});
|
|
};
|
|
|
|
test.describe('M2 #65 Photos-ready e2e (set1-48x2)', () => {
|
|
test('batch → pick → mask → measure (tape+fields) → photos ready + screenshots', async ({ page }) => {
|
|
test.setTimeout(180_000);
|
|
expect(fs.existsSync(FACE_A), 'fixture face-a').toBeTruthy();
|
|
expect(fs.existsSync(FACE_B), 'fixture face-b').toBeTruthy();
|
|
|
|
await ensureFreshBatch(page);
|
|
|
|
// Gallery multi-input (not the capture=environment camera input).
|
|
const gallery = page.locator('input[type="file"][multiple]');
|
|
await gallery.setInputFiles([FACE_A, FACE_B]);
|
|
await expect(page.getByAltText('Photo 1')).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByAltText('Photo 2')).toBeVisible();
|
|
await shot(page, '01-batch.png');
|
|
|
|
await page.getByRole('button', { name: 'Continue' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Pick calibration photo' })).toBeVisible();
|
|
await shot(page, '02-pick-calibration.png');
|
|
|
|
await page.getByRole('button', { name: 'Use photo 1 as calibration' }).click();
|
|
await prepareMaskForApprove(page);
|
|
await shot(page, '03-mask.png');
|
|
await page.getByRole('button', { name: 'Approve mask' }).click();
|
|
|
|
await expect(page.getByRole('heading', { name: 'Measure' })).toBeVisible();
|
|
await shot(page, '04-tape-measure.png');
|
|
await page.getByLabel('Length (in)').fill('48');
|
|
await page.getByLabel('Thickness').fill('2');
|
|
// blur/change commits eighths into session before Calculate.
|
|
await page.getByLabel('Length (in)').blur();
|
|
await page.getByLabel('Thickness').blur();
|
|
await shot(page, '05-measure.png');
|
|
|
|
const calc = page.getByRole('button', { name: 'Calculate' });
|
|
await expect(calc).toBeEnabled();
|
|
await calc.click();
|
|
|
|
// Calculate encodes calibration PNG + overlay + sibling batch apply.
|
|
await expect(page.getByRole('heading', { name: 'Photos ready' })).toBeVisible({
|
|
timeout: 120_000,
|
|
});
|
|
await expect(page.getByLabel('Photos ready')).toBeVisible();
|
|
await expect(page.getByText('Reference photo')).toBeVisible();
|
|
await expect(page.getByText('Photo with measurements')).toBeVisible();
|
|
// Sibling face-b should appear as Photo 2 (capture order).
|
|
await expect(page.getByText(/Photo 2|Photo 2 failed/)).toBeVisible();
|
|
|
|
await shot(page, '06-photos-ready.png');
|
|
|
|
// Write a short run log for parent AI visual review (not a substitute for eyes).
|
|
const logPath = path.join(ARTIFACTS, 'run-log.txt');
|
|
const shots = ['01-batch.png', '02-pick-calibration.png', '03-mask.png', '04-tape-measure.png', '05-measure.png', '06-photos-ready.png'];
|
|
const lines = [
|
|
`M2 #65 Playwright e2e — set1-48x2 (L=48, thickness=2)`,
|
|
`baseURL: ${page.url().split('/').slice(0, 3).join('/')}`,
|
|
`finished: ${new Date().toISOString()}`,
|
|
`artifacts:`,
|
|
...shots.map((s) => {
|
|
const p = path.join(ARTIFACTS, s);
|
|
const st = fs.existsSync(p) ? fs.statSync(p) : null;
|
|
return ` - ${s} (${st ? `${st.size} bytes` : 'MISSING'})`;
|
|
}),
|
|
`assertions: Photos ready heading; Reference photo; Photo with measurements; batch sibling slot`,
|
|
];
|
|
fs.writeFileSync(logPath, lines.join('\n') + '\n', 'utf8');
|
|
});
|
|
});
|