← back to Quadrille Showroom

proto/shot-colorriver.cjs

89 lines

#!/usr/bin/env node
/* Headless verifier for proto/v6-colorriver.html
   - boots the proto static+proxy server (proxy-server.cjs) on a temp port
   - loads /v6-colorriver.html in chromium
   - waits for swatches to load, captures console errors
   - asserts swatches rendered AND sorted by hue
   - writes proto/shots/v6-colorriver.png
   Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node proto/shot-colorriver.cjs
*/
const { spawn } = require("child_process");
const path = require("path");
const http = require("http");

const PROTO_DIR = __dirname;
const PORT = 7711;
const URL = `http://127.0.0.1:${PORT}/v6-colorriver.html`;

function waitForServer(port, tries=40){
  return new Promise((resolve,reject)=>{
    const probe=(n)=>{
      const req=http.get({host:"127.0.0.1",port,path:"/v6-colorriver.html"},(r)=>{r.destroy();resolve();});
      req.on("error",()=>{ if(n<=0) return reject(new Error("server never came up")); setTimeout(()=>probe(n-1),150); });
    };
    probe(tries);
  });
}

(async()=>{
  let pw;
  try { pw = require("playwright"); } catch(e){ console.error("NO playwright:",e.message); process.exit(2); }

  // start proto server
  const srv = spawn("node", [path.join(PROTO_DIR,"proxy-server.cjs")], {
    env:{ ...process.env, PROTO_PORT:String(PORT) }, stdio:"inherit"
  });
  let exitCode = 1;
  try {
    await waitForServer(PORT);
    const browser = await pw.chromium.launch({ headless:true });
    const page = await browser.newPage({ viewport:{ width:1440, height:900 }, deviceScaleFactor:2 });

    const errors = [];
    page.on("console", m=>{ if(m.type()==="error") errors.push(m.text()); });
    page.on("pageerror", e=>errors.push("PAGEERROR: "+e.message));

    await page.goto(URL, { waitUntil:"domcontentloaded", timeout:30000 });

    // wait until full load+sort completes (or 35s)
    await page.waitForFunction(()=>window.__RIVER__ && window.__RIVER__.done(), { timeout:35000 })
      .catch(()=>{});

    // let textures settle + boot fade
    await page.waitForTimeout(2000);

    const count = await page.evaluate(()=>window.__RIVER__ ? window.__RIVER__.count() : 0);
    const sorted = await page.evaluate(()=>window.__RIVER__ ? window.__RIVER__.sortedByHue() : false);
    const spatial = await page.evaluate(()=>window.__RIVER__ ? window.__RIVER__.spatialSorted() : false);
    const hueSample = await page.evaluate(()=>{
      const h = window.__RIVER__ ? window.__RIVER__.hues() : [];
      return { first:h.slice(0,5), last:h.slice(-5), n:h.length };
    });

    const shotPath = path.join(PROTO_DIR,"shots","v6-colorriver.png");
    await page.screenshot({ path:shotPath });

    console.log("\n================ COLOR RIVER VERIFY ================");
    console.log("swatches rendered :", count);
    console.log("sorted by hue     :", sorted);
    console.log("spatial X by hue  :", spatial);
    console.log("hue range (sorted):", hueSample.first, "...", hueSample.last);
    console.log("console errors    :", errors.length);
    if(errors.length) errors.slice(0,10).forEach(e=>console.log("   !", e));
    console.log("screenshot        :", shotPath);
    console.log("===================================================\n");

    await browser.close();

    const ok = count >= 40 && sorted === true && spatial === true && errors.length === 0;
    exitCode = ok ? 0 : 1;
    console.log(ok ? "VERDICT: PASS" : "VERDICT: FAIL");
  } catch(e){
    console.error("verify error:", e);
    exitCode = 3;
  } finally {
    srv.kill("SIGTERM");
    setTimeout(()=>process.exit(exitCode), 300);
  }
})();