// Garment-capture review step check (issue #435 phase 1) — local preview mock harness. // Intercepts the upload + PUT endpoints, injects a FULL StoredUser persona, and asserts // the AI review step: role-chip dropdowns (app-native), editable tag fields, low-confidence // markers, Save→PUT, Skip→no-PUT. Screenshots to ../.qa-screens/. const { chromium } = require('/root/.npm/_npx/e41f203b7505f1fb/node_modules/playwright'); const fs = require('fs'); const path = require('path'); const chromiumDirs = fs.readdirSync('/root/.cache/ms-playwright').filter(d => d.startsWith('chromium') && !d.includes('headless_shell')); const CHROME = path.join('/root/.cache/ms-playwright', chromiumDirs[chromiumDirs.length - 1], 'chrome-linux64', 'chrome'); const BASE = 'http://localhost:4173'; const OUT = path.resolve(__dirname, '..', '.qa-screens'); fs.mkdirSync(OUT, { recursive: true }); // 1x1 transparent PNG const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64'); // Full StoredUser persona with a permissions map covering the library module. const USER = { id: 'u1', email: 'pratik@sonalapparels.in', name: 'Pratik Sabat', role: 'Admin', scope: 'all', is_active: true, department: { id: 'dept-1', name: 'Design' }, permissions: { library: 'manage', boards: 'manage', moodboard: 'manage', trends: 'manage', analytics: 'view', review: 'manage', ingest: 'manage', inbox: 'view', tna: 'view', users: 'none', data: 'none', }, }; // Upload fixture with shot_roles + field_confidences (one field < 85). const UPLOAD_FIXTURE = { garment_id: 'g-capture-1', style_number: 'W0042', review_status: 'in_review', tags: { auto_saved: { silhouette: { value: 'Regular', confidence: 0.92 }, season: { value: 'AW26', confidence: 0.95 }, }, needs_review: { neckline: { value: 'Round neck', confidence: 0.7 }, }, rejected: {}, }, tagging_notes: ['Test model note'], shot_roles: [ { index: 0, path: '/images/a.jpg', role: 'front', confidence: 90 }, { index: 1, path: '/images/b.jpg', role: 'back', confidence: 85 }, ], field_confidences: { silhouette: 92, season: 95, neckline: 70 }, possible_duplicates: [], similar: [], capture_mode: 'gallery', capture_quality: [ { index: 0, bg_removed: true, depth_score: 70, texture_quality: 'Good', blur_warning: false }, { index: 1, bg_removed: true, depth_score: 55, texture_quality: 'Good', blur_warning: false }, ], }; let failures = []; function check(name, ok, detail = '') { console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); if (!ok) failures.push(name); } async function setupRoutes(page, puts) { // Catch-all logger first so specific routes registered after take precedence. await page.route('**/api/**', (r) => { const url = r.request().url(); if (/\/garments\/upload/.test(url)) return r.fulfill({ status: 201, json: UPLOAD_FIXTURE }); console.log(`ROUTE other: ${r.request().method()} ${url}`); return r.continue(); }); // Boilerplate shell APIs. await page.route('**/api/notifications/unread-count*', r => r.fulfill({ json: { count: 0 } })); await page.route('**/api/notifications*', r => r.fulfill({ json: [] })); await page.route('**/api/auth/me*', r => r.fulfill({ json: { user: USER } })); // next-style-number is POSTed when a file is added. await page.route('**/api/plm/next-style-number*', r => r.fulfill({ json: { style_number: 'W0042', segment: 'new_dev', category: 'womens', reserved: false } })); // Upload endpoint — handled by the garments glob below; keep only that. // Upload route (explicit, registered after the garments glob for precedence). await page.route('**/api/garments/upload*', (r) => { console.log(`ROUTE upload2: ${r.request().method()} ${r.request().url()}`); return r.fulfill({ status: 201, json: UPLOAD_FIXTURE }); }); await page.route('**/api/garments/*', async (r) => { const method = r.request().method(); const url = r.request().url(); if (/\/garments\/upload/.test(url)) { console.log(`ROUTE upload: ${method} ${url}`); return r.fulfill({ status: 201, json: UPLOAD_FIXTURE }); } if (method === 'PUT') { puts.push({ url, body: r.request().postDataJSON() }); return r.fulfill({ json: UPLOAD_FIXTURE }); } return r.continue(); }); // Upload route (explicit, registered after the garments glob for precedence). await page.route('**/api/garments/upload*', (r) => { console.log(`ROUTE upload2: ${r.request().method()} ${r.request().url()}`); return r.fulfill({ status: 201, json: UPLOAD_FIXTURE }); }); } async function injectAuth(page) { await page.evaluate((u) => { localStorage.setItem('sapl_token', 'test-token'); localStorage.setItem('sapl_user', JSON.stringify(u)); }, USER); } async function setShots(page) { const inputs = page.locator('input[type="file"]'); for (let i = 0; i < 2; i++) await inputs.nth(i).setInputFiles({ name: `shot${i}.png`, mimeType: 'image/png', buffer: PNG }); } async function runScenario(label, viewport, opts = {}) { const browser = await chromium.launch({ executablePath: CHROME }); try { const ctx = await browser.newContext({ viewport }); const page = await ctx.newPage(); const puts = []; await setupRoutes(page, puts); await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded' }); await injectAuth(page); await page.goto(BASE + '/upload', { waitUntil: 'domcontentloaded' }); await setShots(page); await page.waitForTimeout(300); await page.locator('button:has-text("Process")').first().click({ timeout: 15000 }); const modal = page.locator('[data-testid=review-ai-modal]'); await modal.waitFor({ state: 'visible', timeout: 15000 }); check(`${label}: review step opens after upload`, true); // No native