- Review to Ready
+ Review slab
Length {fmt1(capture?.lengthIn ?? slab?.length_in ?? 0)}″
Thickness {fmt1(capture?.thicknessIn ?? slab?.thickness_in ?? 0)}″
-
Sqft / Bdft {fmt2(capture?.sqft ?? slab?.sqft ?? 0)} / {fmt2(capture?.bdft ?? slab?.bdft ?? 0)}
+
Square feet / Board feet {fmt2(capture?.sqft ?? slab?.sqft ?? 0)} / {fmt2(capture?.bdft ?? slab?.bdft ?? 0)}
@@ -767,12 +775,18 @@
{i + 1}
-
+
+ {#if p.required}
+
+
+
+ {/if}
+
{/each}
{#if lineupMissing.length}
-
+
Keep at least {lineupMissing.map(roleLabel).join(' and ')} — required to publish.
{/if}
@@ -780,7 +794,7 @@
{/if}
-
+
@@ -798,7 +812,7 @@
/>
{#if primaryOpts.length}
-
Primary wood category drives title
+
Primary wood category used for title
{#each primaryOpts as o (o.id)}
{/each}
- Primary selects the title & short-description pattern (METADATA-85).
- Secondary categories feed the description. Default = first selected.
+ Primary is used for the title. Other selected categories can appear in the description.
{/if}
@@ -898,11 +911,11 @@
- {createBusy ? 'Building…' : 'Create short desc'}
+ {createBusy ? 'Building…' : 'Create short description'}
{#if !canCreateTitle}
-
+
Create title needs: {createTitleMissing.join(', ')}
{/if}
@@ -917,29 +930,31 @@
data-testid="create-desc"
on:click={generateContent}
disabled={!canCreateDesc || genBusy}
- title={canCreateDesc ? 'Generate short + long description (Call 2)' : `Missing: ${createDescMissing.join(', ')}`}
+ title={canCreateDesc ? 'Generate short and full description' : `Missing: ${createDescMissing.join(', ')}`}
>
- {genBusy ? 'Writing…' : 'Create desc'}
-
- { if (viewPromptOpen) viewPromptOpen = false; else void loadViewPrompt(); }}
- disabled={viewPromptBusy}
- >
- {viewPromptBusy ? 'Loading…' : (viewPromptOpen ? 'Hide prompt' : 'View prompt')}
+ {genBusy ? 'Writing…' : 'Create description'}
+ {#if showViewPrompt}
+ { if (viewPromptOpen) viewPromptOpen = false; else void loadViewPrompt(); }}
+ disabled={viewPromptBusy}
+ >
+ {viewPromptBusy ? 'Loading…' : (viewPromptOpen ? 'Hide prompt' : 'View prompt')}
+
+ {/if}
{#if !canCreateDesc}
-
- Create desc needs: {createDescMissing.join(', ')}
+
+ Create description needs: {createDescMissing.join(', ')}
{/if}
{#if viewPromptOpen}
-
Call 2 prompt (read-only)
+
Prompt (read-only)
{viewPromptCopied ? 'Copied' : 'Copy text'}
@@ -949,15 +964,13 @@
{#if viewPrompt}
{viewPrompt.text}
-
Photo slots (no base64) — up to 8 processed PNGs
+
Photos included
{#if viewPrompt.photos.length === 0}
-
No processed photos on this slab.
+
No photos on this slab yet.
{:else}
- {#each viewPrompt.photos as ph (ph.id)}
-
- seq={ph.seq} · id={ph.id} · {ph.bytes} bytes · long-edge {ph.long_edge_px ?? '?'}px
-
+ {#each viewPrompt.photos as ph, i (ph.id)}
+ Photo {i + 1}
{/each}
{/if}
@@ -977,10 +990,10 @@
-
SKU last field, 3–24 chars
+
SKU 3–24 letters, numbers, or hyphens
{#if sku && !/^[A-Za-z0-9-]{3,24}$/.test(sku)}
-
SKU must be 3–24 alphanumerics or hyphens.
+
SKU must be 3–24 letters, numbers, or hyphens.
{/if}
@@ -997,7 +1010,7 @@
].length} field${[...review.missing, ...lineupMissing.map(roleLabel)].length === 1 ? '' : 's'} remaining`}
{#if !(ready() && lineupValid)}
-
Missing: {[...review.missing, ...lineupMissing.map(roleLabel)].join(', ')}
+
Missing: {[...review.missing, ...lineupMissing.map(roleLabel)].join(', ')}
{/if}
{/if}
{#if reviewError}
{reviewError}
{/if}
@@ -1031,7 +1044,8 @@
.sel { appearance: none; background-image: linear-gradient(45deg,transparent 50%,#777 50%),linear-gradient(135deg,#777 50%,transparent 50%); background-position: calc(100% - 16px) 55%,calc(100% - 11px) 55%; background-size: 5px 5px; background-repeat: no-repeat; }
.ta { resize: vertical; }
.field { margin-bottom: 12px; }
- .nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; }
+ .nudge { color: #aaa; font-size: .74rem; margin: 4px 0 0; }
+ .nudge.must, .must { color: #ff6b6b; }
.sug { color: #aaa; font-size: .74rem; font-weight: 400; margin-left: 6px; }
.gen { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; }
/* Unified secondary actions: Create title/short/desc, View prompt — same size & label weight. */
@@ -1066,9 +1080,9 @@
.ps-opt.on .ps-radio::after { content: ''; position: absolute; inset: 2px; border-radius: 50%; background: #00e5ff; }
.ps-name { flex: 1; }
.ps-note { font-size: .7rem; color: #777; margin: 6px 0 0; line-height: 1.4; }
- .status { margin-top: 14px; font-weight: 700; padding: 10px 12px; border-radius: 3px; background: #2a1512; color: #ffb4a2; font-size: .9rem; }
+ .status { margin-top: 14px; font-weight: 700; padding: 10px 12px; border-radius: 3px; background: #2a1512; color: #ff6b6b; font-size: .9rem; }
.status.ok { background: #122a12; color: #7fd37f; }
- .missing { color: #999; font-size: .76rem; margin: 6px 0 0; }
+ .missing { color: #ff6b6b; font-size: .76rem; margin: 6px 0 0; }
.row { display: flex; gap: 10px; margin-top: 16px; }
.primary { flex: 1; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0; background: #00e5ff; color: #002; font-weight: 700; cursor: pointer; }
.primary:disabled { background: #333; color: #777; cursor: not-allowed; }
@@ -1080,7 +1094,7 @@
.ph-hint { color: #8a8a8a; font-size: .72rem; margin: 0 0 8px; }
.ph-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
@media (max-width: 420px) { .ph-grid { grid-template-columns: repeat(2, 1fr); } }
- .pcard { position: relative; aspect-ratio: 1; border: 2px solid #2a2a2a; border-radius: 3px; overflow: hidden; cursor: grab;
+ .pcard { position: relative; aspect-ratio: 3 / 4; border: 2px solid #2a2a2a; border-radius: 3px; overflow: hidden; cursor: grab;
/* Gray checkerboard, matching the Capture lineup (MeasurePanel .lineup-item.png)
so transparent inventory PNGs read the same on both screens (bug2). */
background-color: #eee;
@@ -1091,28 +1105,24 @@
.pcard.kept { border-color: #444; }
.pcard.primary { border-color: #00e5ff; }
.pcard:not(.kept) { opacity: .4; }
- .ph-thumb { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
+ .ph-thumb { width: 100%; height: 100%; object-fit: contain; display: block; pointer-events: none; }
.ph-pri { position: absolute; top: 6px; right: 6px; background: #00e5ff; color: #002; font-size: .56rem; font-weight: 800; padding: 1px 5px; border-radius: 2px; letter-spacing: .03em; pointer-events: none; }
.ph-req { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); background: #ffb020; color: #201500; font-size: .52rem; font-weight: 800; padding: 1px 5px; border-radius: 2px; letter-spacing: .04em; pointer-events: none; }
.ph-seq { position: absolute; bottom: 6px; left: 6px; width: 18px; height: 18px; border-radius: 50%; background: rgba(0,0,0,.7); color: #fff; font-size: .66rem; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.ph-keep { position: absolute; top: 6px; left: 6px; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 3px; background: rgba(0,0,0,.5); pointer-events: none; }
.pcard.kept .ph-keep { background: #00e5ff; border-color: #00e5ff; }
.pcard.kept .ph-keep::after { content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #002; border-width: 0 2px 2px 0; transform: rotate(45deg); }
- /* Mandatory photos (clean cal + dimension-overlay): the keep checkbox is
- checked AND disabled. A bold padlock glyph in the box signals "required —
- cannot be removed", and the card carries a thin required outline so the
- owner can tell mandatory photos apart from optional batch photos. */
+ /* Mandatory photos (reference + measurements): keep is locked; SVG lock
+ signals required / cannot remove. */
.pcard.required { border-color: #00e5ff55; }
- .pcard.required .ph-keep { background: #00e5ff; border-color: #00e5ff; }
- /* Padlock: shackle (arc) + body, drawn large enough to read on a 16px box. */
- .pcard.required .ph-keep::after {
- content: ''; position: absolute; left: 3px; top: 2px; width: 8px; height: 8px;
- border: 2px solid #002; border-bottom: none; border-radius: 4px 4px 0 0;
- }
- .pcard.required .ph-keep::before {
- content: ''; position: absolute; left: 2.5px; top: 6px; width: 8px; height: 6px;
- background: #002; border-radius: 1px;
+ .pcard.required .ph-keep {
+ background: #00e5ff; border-color: #00e5ff; color: #002;
+ display: flex; align-items: center; justify-content: center;
}
+ .pcard.required .ph-keep::after { content: none; }
+ .pcard.required .ph-keep::before { content: none; }
+ .pcard.required.kept .ph-keep::after { content: none; }
+ .ph-keep .lock { display: block; }
.pcard.required { cursor: default; }
- .ph-nudge { color: #e8a; font-size: .74rem; margin: 8px 0 0; }
+ .ph-nudge { color: #ff6b6b; font-size: .74rem; margin: 8px 0 0; }
diff --git a/frontend/src/create/review/createDescGate.test.ts b/frontend/src/create/review/createDescGate.test.ts
index 9b1668e..1d91542 100644
--- a/frontend/src/create/review/createDescGate.test.ts
+++ b/frontend/src/create/review/createDescGate.test.ts
@@ -66,7 +66,7 @@ test('Create desc gate: lists each missing field; no short/description/price key
assert.equal(labels.includes('price'), false);
});
-test('Create desc gate: inference URL+key required even when content LLM on', () => {
+test('Create desc gate: inference URL+key required even when description writing on', () => {
assert.equal(canCreateDesc({ ...full, inferenceConfigured: false }), false);
assert.deepEqual(missingCreateDesc({ ...full, inferenceConfigured: false }), [
'inference_creds',
diff --git a/frontend/src/create/review/createDescGate.ts b/frontend/src/create/review/createDescGate.ts
index d3880c2..9994312 100644
--- a/frontend/src/create/review/createDescGate.ts
+++ b/frontend/src/create/review/createDescGate.ts
@@ -23,8 +23,8 @@ export type CreateDescInput = {
/** Human labels shown under Create desc when the gate fails. */
export const CREATE_DESC_LABELS: Record
= {
- content_llm: 'content LLM on',
- inference_creds: 'inference URL + API key',
+ content_llm: 'description writing on',
+ inference_creds: 'description connection',
title: 'title',
species: 'species',
wood_categories: '≥1 wood category',
diff --git a/frontend/src/create/review/lineup.test.ts b/frontend/src/create/review/lineup.test.ts
index 27be45d..ebd3f94 100644
--- a/frontend/src/create/review/lineup.test.ts
+++ b/frontend/src/create/review/lineup.test.ts
@@ -67,8 +67,8 @@ test('lineup: missingMinimums only counts KEPT photos', () => {
});
test('lineup: roleLabel is owner-facing', () => {
- assert.match(roleLabel('calibration'), /clean/);
- assert.match(roleLabel('overlay'), /overlay/);
+ assert.match(roleLabel('calibration'), /reference photo/i);
+ assert.match(roleLabel('overlay'), /measurements/i);
});
test('lineup: resolvePhotoUrl turns an in-memory blob into a blob: URL (bug #1)', () => {
diff --git a/frontend/src/create/review/lineup.ts b/frontend/src/create/review/lineup.ts
index bc931c8..cbd8df4 100644
--- a/frontend/src/create/review/lineup.ts
+++ b/frontend/src/create/review/lineup.ts
@@ -57,9 +57,9 @@ export function lineupValid(kept: LineupItem[]): boolean {
/** A short owner-facing label for a role (used in the minimum nudge). */
export function roleLabel(role: PhotoRole): string {
- if (role === 'calibration') return 'a clean photo';
- if (role === 'overlay') return 'its dimension-overlay copy';
- return 'a batch photo';
+ if (role === 'calibration') return 'the reference photo';
+ if (role === 'overlay') return 'the photo with measurements';
+ return 'a photo';
}
/**
diff --git a/frontend/src/create/review/reviewUi.contract.test.ts b/frontend/src/create/review/reviewUi.contract.test.ts
index 1aab880..c022b29 100644
--- a/frontend/src/create/review/reviewUi.contract.test.ts
+++ b/frontend/src/create/review/reviewUi.contract.test.ts
@@ -58,3 +58,19 @@ test('Review UI: live field commit + unified secondary buttons + moisture defaul
assert.equal(reviewSrc.includes('create-btn'), false);
});
+test('Review UI: Jeff-facing copy + 3:4 thumbs + View prompt gated', () => {
+ assert.match(reviewSrc, /Review slab/);
+ assert.match(reviewSrc, /Square feet \/ Board feet/);
+ assert.match(reviewSrc, /Create short description/);
+ assert.match(reviewSrc, /Create description/);
+ assert.match(reviewSrc, /Generate short and full description/);
+ assert.match(reviewSrc, /3–24 letters, numbers, or hyphens/);
+ assert.match(reviewSrc, /aspect-ratio: 3 \/ 4/);
+ assert.match(reviewSrc, /object-fit: contain/);
+ assert.match(reviewSrc, /getShowViewPrompt/);
+ assert.match(reviewSrc, /showViewPrompt/);
+ assert.equal(reviewSrc.includes('Call 2'), false);
+ assert.equal(reviewSrc.includes('Review to Ready'), false);
+ assert.equal(/aspect-ratio:\s*1\b/.test(reviewSrc), false);
+});
+
diff --git a/frontend/src/create/settings/Settings.svelte b/frontend/src/create/settings/Settings.svelte
index c53e567..5f7f8d7 100644
--- a/frontend/src/create/settings/Settings.svelte
+++ b/frontend/src/create/settings/Settings.svelte
@@ -9,6 +9,7 @@
withWooHeaders,
} from '../../api/wooDeviceCreds';
import { reopenWooGate, isWooAuthFailure } from '../../api/wooGate';
+ import { getShowViewPrompt, setShowViewPrompt } from '../../settings/viewPrompt';
interface SecretState { configured: boolean }
// Masked mode: object {configured}. Unmasked UAT mode (SLAB_UAT_UNMASK_SECRETS): plain string.
@@ -52,6 +53,7 @@
// This device only — localStorage; never sent via Settings PUT.
let deviceWoo = { username: '', appPassword: '' };
let deviceMsg = '';
+ let showViewPrompt = getShowViewPrompt();
let rateRows: RateRow[] = [];
let ratesLoading = false;
@@ -409,6 +411,19 @@
Content LLM Enabled (default false)
+
+
Review UI
+
+ (showViewPrompt = setShowViewPrompt(e.currentTarget.checked))}
+ />
+ Show “View prompt” on Review (default off)
+
+
+
Brand Voice
diff --git a/frontend/src/png/overlay.test.ts b/frontend/src/png/overlay.test.ts
index 74519c4..2065f25 100644
--- a/frontend/src/png/overlay.test.ts
+++ b/frontend/src/png/overlay.test.ts
@@ -5,6 +5,7 @@ import {
pickBottomTowardTop,
axisRightUnit,
buildOverlayPlan,
+ OVERLAY_FONT_PX,
inventorySourceMap,
BODY_WIDTH_FRAC,
type OverlayPlan,
@@ -139,7 +140,8 @@ test('buildOverlayPlan: length line on axis-right outside slab; widths at 12in s
// Color contract for draw consumers
assert.equal(plan.stroke, '#000000');
- assert.ok(plan.fontPx >= 12, `fontPx ${plan.fontPx} large enough to read`);
+ assert.equal(plan.fontPx, OVERLAY_FONT_PX);
+ assert.ok(plan.fontPx >= 28, `fontPx ${plan.fontPx} large enough to read`);
});
test('buildOverlayPlan: stations measured from EXIF bottom, not tip-inclusive M1 every-6 (#63)', () => {
@@ -253,8 +255,8 @@ test('buildOverlayPlan: width labels sit axis-left outside the slab (#67)', () =
for (const w of plan.widths) {
assert.ok(w.labelAt, 'width mark must expose labelAt');
// left/right tips still on wood edges
- assert.ok(Math.abs(w.left[1] - 149) <= 2 || Math.abs(w.left[1] - 50) <= 2);
- assert.ok(Math.abs(w.right[1] - 149) <= 2 || Math.abs(w.right[1] - 50) <= 2);
+ assert.ok(Math.abs(w.left[1] - 149) <= 5 || Math.abs(w.left[1] - 50) <= 5);
+ assert.ok(Math.abs(w.right[1] - 149) <= 5 || Math.abs(w.right[1] - 50) <= 5);
// labelAt on axis-left = opposite of axis-right from center
// For this geometry, axis-right is toward smaller y; axis-left toward larger y
// (outside past the bottom edge y≈149).
@@ -273,29 +275,36 @@ test('buildOverlayPlan: width labels sit axis-left outside the slab (#67)', () =
assert.ok(lenY < 50 - 4, `length dim stays axis-right outside top edge, got y=${lenY}`);
});
-test('buildOverlayPlan: font smaller than prior 28/scale*1.2 and 22/scale*1.0 (#67,#71)', () => {
+test('buildOverlayPlan: fixed readable measurement font for every product', () => {
const W = 480, H = 200;
const bitset = new Uint8Array(W * H);
for (let y = 50; y < 150; y++) for (let x = 0; x < W; x++) bitset[y * W + x] = 1;
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
- const scale = 10;
- const plan = buildOverlayPlan({
+ const planA = buildOverlayPlan({
bitset,
widthPx: W,
heightPx: H,
bbox: box,
- scale,
+ scale: 10,
lengthIn: 48,
lengthLabel: '48"',
mapToInventory: (x, y) => [x, y],
});
- const oldFont67 = Math.max(28, Math.round(scale * 1.2));
- const oldFont71 = Math.max(22, Math.round(scale * 1.0));
- assert.ok(plan.fontPx < oldFont67, `fontPx ${plan.fontPx} must be < prior ${oldFont67}`);
- assert.ok(plan.fontPx < oldFont71, `fontPx ${plan.fontPx} must be < prior ${oldFont71}`);
- assert.ok(plan.fontPx >= 12, `fontPx ${plan.fontPx} still legible`);
- // Concrete contract (#71): capped by scale when no inventory size
- assert.equal(plan.fontPx, Math.max(12, Math.min(16, Math.round(scale * 0.18))));
+ const planB = buildOverlayPlan({
+ bitset,
+ widthPx: W,
+ heightPx: H,
+ bbox: box,
+ scale: 80,
+ lengthIn: 48,
+ lengthLabel: '48"',
+ mapToInventory: (x, y) => [x, y],
+ inventoryW: 1200,
+ inventoryH: 1600,
+ });
+ assert.equal(planA.fontPx, OVERLAY_FONT_PX);
+ assert.equal(planB.fontPx, OVERLAY_FONT_PX);
+ assert.ok(planA.fontPx >= 28, `fontPx ${planA.fontPx} must be large enough to read`);
});
@@ -908,9 +917,8 @@ test('buildOverlayPlan: labels inside inventory PNG; length off wood; smaller fo
});
// Font: one consistent smaller size than prior max(22, scale*1.0)
- const priorFont = Math.max(22, Math.round(scale * 1.0));
- assert.ok(plan.fontPx < priorFont, `fontPx ${plan.fontPx} must be < prior ${priorFont}`);
- assert.ok(plan.fontPx >= 12, `fontPx ${plan.fontPx} still readable`);
+ assert.equal(plan.fontPx, OVERLAY_FONT_PX);
+ assert.ok(plan.fontPx >= 28, `fontPx ${plan.fontPx} still readable`);
// Wood AABB in inventory
let mMinX = Infinity, mMaxX = -Infinity, mMinY = Infinity, mMaxY = -Infinity;
diff --git a/frontend/src/png/overlay.ts b/frontend/src/png/overlay.ts
index b1cf9ad..9e87b0e 100644
--- a/frontend/src/png/overlay.ts
+++ b/frontend/src/png/overlay.ts
@@ -262,6 +262,9 @@ function probeExtent(
return { minP, maxP };
}
+/** Jeff-facing dimension labels: fixed size in inventory PNG pixels. */
+export const OVERLAY_FONT_PX = 36;
+
function fmtInches(inches: number): string {
const whole = Math.floor(inches + 1e-9);
const rem = Math.round((inches - whole) * 8);
@@ -286,14 +289,9 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
const { bottom, top, ux, uy } = oriented;
const [rx, ry] = axisRightUnit(ux, uy);
- // Consistent smaller readable font (#71). Cap by inventory size so high
- // source scale (≈80 px/in) does not dominate the PNG.
- const fontPx = (() => {
- if (inventoryW && inventoryH && inventoryW > 0 && inventoryH > 0) {
- return Math.max(12, Math.min(16, Math.round(Math.min(inventoryW, inventoryH) * 0.016)));
- }
- return Math.max(12, Math.min(16, Math.round(scale * 0.18)));
- })();
+ // Fixed readable measurement font in inventory image pixels (Jeff-facing).
+ // Same size for every product — do not scale with tape px/in or canvas size.
+ const fontPx = OVERLAY_FONT_PX;
// Inset for stroke + arrowhead so visual tips sit on/inside edges (#71).
const lineW = Math.max(2, Math.round(fontPx / 10));
const arrowHead = Math.max(7, fontPx * 0.32);
diff --git a/frontend/src/settings/viewPrompt.ts b/frontend/src/settings/viewPrompt.ts
new file mode 100644
index 0000000..2b70fbb
--- /dev/null
+++ b/frontend/src/settings/viewPrompt.ts
@@ -0,0 +1,29 @@
+/**
+ * Device-local toggle: show Review "View prompt" (default off).
+ * Jeff-facing UI leaves this hidden unless an operator enables it in Settings.
+ */
+const KEY = 'slabuploader.ui.show_view_prompt';
+export const VIEW_PROMPT_EVENT = 'slabuploader:show-view-prompt';
+
+export function getShowViewPrompt(): boolean {
+ try {
+ return localStorage.getItem(KEY) === '1';
+ } catch {
+ return false;
+ }
+}
+
+export function setShowViewPrompt(on: boolean): boolean {
+ try {
+ if (on) localStorage.setItem(KEY, '1');
+ else localStorage.removeItem(KEY);
+ } catch {
+ /* ignore quota / private mode */
+ }
+ try {
+ window.dispatchEvent(new CustomEvent(VIEW_PROMPT_EVENT, { detail: on }));
+ } catch {
+ /* ignore non-browser */
+ }
+ return on;
+}
diff --git a/frontend/tests/e2e/photos-ready.spec.ts b/frontend/tests/e2e/photos-ready.spec.ts
index fcff5d7..4a09878 100644
--- a/frontend/tests/e2e/photos-ready.spec.ts
+++ b/frontend/tests/e2e/photos-ready.spec.ts
@@ -22,7 +22,7 @@ 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: 'Batch photos' });
+ 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();
@@ -74,8 +74,8 @@ test.describe('M2 #65 Photos-ready e2e (set1-48x2)', () => {
// 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('Batch photo 1')).toBeVisible({ timeout: 30_000 });
- await expect(page.getByAltText('Batch photo 2')).toBeVisible();
+ 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();
@@ -89,10 +89,10 @@ test.describe('M2 #65 Photos-ready e2e (set1-48x2)', () => {
await expect(page.getByRole('heading', { name: 'Measure' })).toBeVisible();
await shot(page, '04-tape-measure.png');
- await page.getByLabel('Length L').fill('48');
+ 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 L').blur();
+ await page.getByLabel('Length (in)').blur();
await page.getByLabel('Thickness').blur();
await shot(page, '05-measure.png');
@@ -105,10 +105,10 @@ test.describe('M2 #65 Photos-ready e2e (set1-48x2)', () => {
timeout: 120_000,
});
await expect(page.getByLabel('Photos ready')).toBeVisible();
- await expect(page.getByText('Clean calibration')).toBeVisible();
- await expect(page.getByText('Dimension overlay')).toBeVisible();
- // Sibling face-b should appear as Batch photo 2 (capture order).
- await expect(page.getByText(/Batch photo 2|Photo 2 failed/)).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');
@@ -125,7 +125,7 @@ test.describe('M2 #65 Photos-ready e2e (set1-48x2)', () => {
const st = fs.existsSync(p) ? fs.statSync(p) : null;
return ` - ${s} (${st ? `${st.size} bytes` : 'MISSING'})`;
}),
- `assertions: Photos ready heading; Clean calibration; Dimension overlay; batch sibling slot`,
+ `assertions: Photos ready heading; Reference photo; Photo with measurements; batch sibling slot`,
];
fs.writeFileSync(logPath, lines.join('\n') + '\n', 'utf8');
});
diff --git a/frontend/tests/e2e/settings-contrast.spec.ts b/frontend/tests/e2e/settings-contrast.spec.ts
index 373e6ce..8a78df9 100644
--- a/frontend/tests/e2e/settings-contrast.spec.ts
+++ b/frontend/tests/e2e/settings-contrast.spec.ts
@@ -109,7 +109,7 @@ test.describe('Settings contrast (Gitea bug #75 black-on-black)', () => {
test('Regression: SlabList.svelte / Review.svelte / Publish.svelte / GenerateText.svelte remain unchanged (still pass their existing contrast)', async ({ page }) => {
const regressionScreens = [
{ name: 'Slabs (dev)', heading: /slabs/i },
- { name: 'Review (dev)', heading: 'Review to Ready' },
+ { name: 'Review (dev)', heading: 'Review slab' },
{ name: 'Publish (dev)', heading: 'Publish to WooCommerce' },
{ name: 'Generate (dev)', heading: 'Generate text' },
];