// Regression test: brand logo (and all /brand-logos/... images) must load // through Next.js Image optimizer on the tuxedo storefront. // // Pre-fix state: // GET https://water.tuxedocorn.com/_next/image?url=%2Fbrand-logos%2F64294306-...%2Flogo.png // → HTTP 400, body: "url parameter is not allowed" // // Post-fix expectation: // → HTTP 200, content-type: image/* and non-empty body // // Root cause identified: Next.js standalone build is missing the // `.next/standalone/public/` folder. The deploy workflow copies // `.next/static` but not `public/`. Without `public/` in standalone, the // image optimizer can't resolve local-relative URLs. import { setTimeout as sleep } from 'node:timers/promises'; const URL = 'https://water.tuxedocorn.com/tuxedo?_=' + Date.now(); const LOGO_PROXY = 'https://water.tuxedocorn.com/_next/image?url=%2Fbrand-logos%2F64294306-5f42-463d-a5e8-2ad6c81a96de%2Flogo.png&w=256&q=75'; const failures = []; // 1. Hit the image proxy directly const r = await fetch(LOGO_PROXY, { headers: { 'User-Agent': 'regression-brand-logos/1.0' }, }); if (r.status !== 200) { failures.push(`Logo proxy returned HTTP ${r.status} (expected 200)`); } else { const ct = r.headers.get('content-type') ?? ''; if (!ct.startsWith('image/')) failures.push(`Logo proxy content-type was ${ct} (expected image/*)`); const buf = new Uint8Array(await r.arrayBuffer()); if (buf.length < 1000) failures.push(`Logo proxy body too small: ${buf.length} bytes`); console.log(` Logo proxy: HTTP ${r.status} ${ct} ${buf.length}B`); } // 2. Verify via headless browser — visit page, check that the logo actually rendered. const { chromium } = await import('playwright'); const browser = await chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await ctx.newPage(); const failed = []; page.on('response', (resp) => { const u = resp.url(); if (u.includes('brand-logos') && resp.status() >= 400) failed.push(`HTTP ${resp.status()} ${u.slice(0, 120)}`); }); await page.goto(URL, { waitUntil: 'networkidle', timeout: 30000 }); // Scroll to bottom so footer logo loads. await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' })); await sleep(4000); await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); await sleep(2000); const logoInfo = await page.evaluate(() => { const imgs = Array.from(document.querySelectorAll('img')); const logos = imgs.filter((i) => (i.currentSrc || i.src || '').includes('brand-logos')); return logos.map((i) => ({ alt: i.alt.slice(0, 40), currentSrc: (i.currentSrc || i.src || '').slice(0, 130), naturalWidth: i.naturalWidth, naturalHeight: i.naturalHeight, complete: i.complete, visible: i.getBoundingClientRect().height > 0, })); }); console.log('\n LOGO IMAGES ON PAGE:'); for (const l of logoInfo) console.log(' ', JSON.stringify(l)); const anyLogoBlank = logoInfo.some((l) => l.naturalWidth === 0 || !l.complete); if (anyLogoBlank) failures.push(`Logo image(s) failed to render: ${JSON.stringify(logoInfo)}`); if (failed.length > 0) failures.push(`Network failed for brand-logos URLs: ${failed.join(' | ')}`); await browser.close(); if (failures.length > 0) { console.error('\n❌ REGRESSION FAILED:'); for (const f of failures) console.error(' -', f); process.exit(1); } else { console.log('\n✅ Brand logos serve correctly through Next.js Image optimizer.'); }