// e2e helper: seed an in-progress device draft (stage='measure', with results) so // the Resume path restores straight to the Photos-ready result 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 { 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((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, }, ); }