Files
route-commerce/scripts/regression-brand-logos.mjs
Nora b90f0fae52
Deploy to route.crispygoat.com / deploy (push) Successful in 4m17s
fix(tuxedo): copy public/ to standalone, mark storm/founder priority
Three issues caused the 'images don't work, video doesn't play' report on
https://water.tuxedocorn.com/tuxedo:

1. (ROOT CAUSE) Next.js standalone build was missing .next/standalone/public/.
   The deploy workflow copied .next/static to .next/standalone/.next/static
   but never mirrored public/. Without that, the image optimizer returns
   400 'url parameter is not allowed' on every local-relative URL — so the
   brand logo (and favicon, etc.) went blank. Symptom: HTTP 400 on
   /_next/image?url=%2Fbrand-logos%2F64294306-...%2Flogo.png.

   Fixed by:
   - cp -r public/. .next/standalone/public/ on the running server (live)
   - updating .gitea/workflows/deploy.yml to mirror public/ on every deploy
   - adding scripts/regression-brand-logos.mjs so this can't regress silently

2. Storm-field and founder-portrait images rendered as 'loading=lazy' and
   appeared blank above the fold. Marked priority so they preload.

3. Pre-existing build error: Next.js 16.2 RSC bundler treats
   'export type { FieldSession }' re-exports through server-action modules
   as runtime exports ('Export FieldSession doesn't exist in target
   module'). Removed the re-exports in water-log/auth.ts and field.ts;
   field.ts now imports FieldSession directly from @/lib/water-log/session-server.

Verified live (post-fix) on 2026-07-06:
  - regression-brand-logos.mjs: HTTP 200 image/png, header+footer logos
    render with naturalWidth=160
  - diag-home-only.mjs: all 6 images complete=true, natural sizes match
    source (storm 1440x1080, founder 604x340, ear 692x476, etc.)
  - verify-hero-video.mjs: readyState=4, paused=false, currentTime
    advancing 2.86s -> 6.87s over 4s (=30fps; the prior 'video doesn't
    play' was likely user perception — the poster+video look near-
    identical, and 'net::ERR_ABORTED' on the MP4 is normal Chrome
    behavior for parallel byte-range requests)
2026-07-06 13:33:54 -06:00

83 lines
3.4 KiB
JavaScript

// 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 <img> 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.');
}