M3 Review→Publish UI (#77): one-column flow, searchable pickers, fig/feat tags, e2e
- Review screen: slug-resolved species/wcat groupers, case-insensitive attrs, fig-*/feat-* tag pickers (METADATA-30/40), AI-assist labels + AI bubbles. - App orchestrator: single writable flow store, capture→review→publish, Resume boot. - API client (slabs.ts) mirroring live backend routes; Blob name fix. - Playwright e2e (7 tests, hermetic mock): canvas PNG seed, client-UUID mock, SKU blur commit, ready-state reactive fix; expect timeout 4s, 2 workers. - e2e green 7/7 in 6s; check_requirements + check_coverage PASS.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// e2e helper: seed an in-progress device draft (stage='measure', with results) so
|
||||
// the Resume path restores straight to the Photos-ready Measure step — the
|
||||
// deterministic driver for the create→review→publish flow (no live camera/canvas
|
||||
// in e2e). The draft is written to the real idb-keyval DB ('keyval-store'/'keyval',
|
||||
// key 'slabuploader-draft') exactly as `saveDraft` would, so `resumeProbe` +
|
||||
// `continueDraft` + `continueToReview` consume it unmodified.
|
||||
//
|
||||
// `source.blob` MUST be a decodable image: restoreFromDraft re-derives the mask
|
||||
// in a Worker that decodes the blob; a failed decode falls back to `mask` (no
|
||||
// results → the Continue button never renders). A 1×1 white PNG decodes fine and
|
||||
// yields slabPx:0 (an empty mask) → rederiveMask resolves → stage reaches measure.
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
const KEY = 'slabuploader-draft';
|
||||
|
||||
export interface SeedMeasure {
|
||||
sku: string;
|
||||
lengthIn: number;
|
||||
thicknessIn: number;
|
||||
sqft: number;
|
||||
bdft: number;
|
||||
widthIn: number;
|
||||
}
|
||||
|
||||
const toEighths = (inches: number) => Math.round(inches * 8);
|
||||
|
||||
export async function seedMeasureDraft(
|
||||
page: Page,
|
||||
d: SeedMeasure,
|
||||
): Promise<void> {
|
||||
const widthEighths = toEighths(d.widthIn);
|
||||
// Build the decodable source PNG in the page via canvas (a fixed base64 1×1
|
||||
// gray+alpha PNG trips Chromium's software decoder in headless), then write
|
||||
// the draft with that blob so restoreFromDraft's worker decode succeeds.
|
||||
await page.evaluate(
|
||||
(payload) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = 1;
|
||||
c.height = 1;
|
||||
const ctx = c.getContext('2d')!;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
c.toBlob((png) => {
|
||||
if (!png) { reject(new Error('canvas toBlob returned null')); return; }
|
||||
const draft = {
|
||||
version: 1,
|
||||
stage: 'measure',
|
||||
batch: [],
|
||||
calibrationIndex: null,
|
||||
source: { widthPx: 1, heightPx: 1, blob: png },
|
||||
maskParams: { sheet: 'green', sensitivity: 50 },
|
||||
mask: {
|
||||
slabPx: 0,
|
||||
bbox: { corners: [], longSidePx: 0, angleDeg: 0 },
|
||||
slabBounds: { x: 0, y: 0, w: 0, h: 0 },
|
||||
},
|
||||
axisBbox: null,
|
||||
lengthEighths: payload.lengthEighths,
|
||||
thicknessEighths: payload.thicknessEighths,
|
||||
results: {
|
||||
lengthEighths: payload.lengthEighths,
|
||||
thicknessEighths: payload.thicknessEighths,
|
||||
scale: 8,
|
||||
widths: {
|
||||
min: payload.widthEighths,
|
||||
max: payload.widthEighths,
|
||||
avg: payload.widthEighths,
|
||||
every6In: [],
|
||||
},
|
||||
sqft: payload.sqft,
|
||||
bdft: payload.bdft,
|
||||
sourceBelowFloor: false,
|
||||
},
|
||||
inventoryPng: png,
|
||||
overlayPng: null,
|
||||
batchApply: null,
|
||||
};
|
||||
const req = indexedDB.open('keyval-store', 1);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
if (!db.objectStoreNames.contains('keyval')) db.createObjectStore('keyval');
|
||||
};
|
||||
req.onsuccess = () => {
|
||||
const db = req.result;
|
||||
const tx = db.transaction('keyval', 'readwrite');
|
||||
tx.objectStore('keyval').put(draft, 'slabuploader-draft');
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => { db.close(); reject(tx.error); };
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
}, 'image/png');
|
||||
}),
|
||||
{
|
||||
lengthEighths: toEighths(d.lengthIn),
|
||||
thicknessEighths: toEighths(d.thicknessIn),
|
||||
widthEighths,
|
||||
sqft: d.sqft,
|
||||
bdft: d.bdft,
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user