Field notesBrowser engineering

Test a WebGL page without mistaking a hook for proof

A build can succeed while the page remains behind a loader. A JavaScript hook can say “ready” while a shader fails. A screenshot can look correct while a keyboard control is unreachable. A useful smoke test combines evidence rather than promoting one convenient signal into a complete verdict.

This repository contains a direct Chrome DevTools Protocol harness in scripts/smoke.mjs. It is a separate command from the site build. That distinction matters when explaining what has actually been verified.

Start with an evidence ladder

What a passing result can reasonably support
CheckEvidenceStill unknown
Bundle and template buildThose inputs compiledBrowser rendering and interaction
HTTP and asset requestsThe expected resources were servedWhether the resources work together
Runtime diagnosticsThe application reports a stateWhether the report matches visible behavior
Screenshot inspectionAppearance at that state and viewportOther states and interaction sequences
Target-device sessionObserved behavior on that deviceEvery other device or workload

A test should name the proposition it checks. “The fallback article is visible with JavaScript disabled” is more useful than “the site passed.” Keep performance measurements separate from functional assertions when the browser uses software rendering.

Own the browser and the profile you start

A direct CDP runner starts a browser with a debugging endpoint, attaches to a target, and enables the domains it uses. A fresh profile avoids inheriting consent, auth, and project state from someone’s everyday browser.

Use a dedicated temporary profile and an available port. Bind debugging to the local machine, retain the process handle, and close only the browser that the runner created. A blanket command that kills every Chrome process is especially inappropriate when another terminal is reviewing a different page.

Higher-level tools such as Playwright can also use browser flags, explicit waits, and custom timeouts. Direct CDP is the repository’s implementation choice, not proof that other tools conceal failures.

Read diagnostics and evaluation errors

Functions such as __labQ() and __gameQ() return fresh diagnostic objects. They are useful for observing chapter, quality, render count, or network state. A hook’s existence alone does not mean a real frame rendered.

async function evaluate(cdp, expression) {
  const reply = await cdp.send("Runtime.evaluate", {
    expression,
    returnByValue: true,
    awaitPromise: true,
  });
  if (reply.exceptionDetails) {
    throw new Error(reply.exceptionDetails.text || "Page evaluation failed");
  }
  return reply.result.value;
}

const state = await evaluate(cdp, "window.__labQ?.() ?? null");

Runtime.evaluate can execute statements, but the value you receive depends on the expression’s completion value. An immediately invoked function is a reliable way to run several statements and return one serializable result. Repeated top-level const or let declarations can collide in the page’s shared lexical scope; merely changing const to let is not a general fix.

Poll for a meaningful condition

Wait for something the application must actually do. In the Lab, a useful check may combine rendered, a positive frame count, and cameraSettled. Inspect console and shader diagnostics too. A green body class set before rendering would invalidate a test that trusted only the class.

import { setTimeout as delay } from "node:timers/promises";

async function waitForState(read, accepts, timeoutMs = 15000) {
  const deadline = performance.now() + timeoutMs;
  let last = null;
  while (performance.now() < deadline) {
    last = await read();
    if (accepts(last)) return last;
    await delay(200);
  }
  throw new Error(`Condition timed out; last state: ${JSON.stringify(last)}`);
}

await waitForState(
  () => evaluate(cdp, "window.__labQ?.() ?? null"),
  state => state?.rendered && state.frames > 0 && state.cameraSettled
);

This example assumes the transport applies a timeout to each CDP command as well. A polling deadline cannot rescue a read() call that never resolves. Save the last state, current URL, screenshot, console errors, and relevant failed requests when a condition expires.

Control the conditions without hiding the behavior

Background-tab throttling can change a two-page simulation. Headless drivers sometimes disable it so both pages advance. That is useful for a synchronization test, but it does not test the normal hidden-tab lifecycle. Keep a separate test with ordinary scheduling for pause and resume.

Likewise, disabling autoplay restrictions makes sound easier to start but cannot prove that a visitor can enable it. Use a real sound-button action when testing the published interaction. Test failures with a blocked model request or disabled JavaScript rather than only forcing an internal “fallback” variable.

Separate browser contexts isolate cookies and local storage for account tests. Same-partition tabs are required for the local BroadcastChannel transport. Match the isolation to the scenario instead of treating one setup as universally correct.

A check must run to gate a release

The repository exposes npm run build, npm run smoke, and screenshot tooling as separate commands. At the start of this revision, the GitHub Pages workflow ran the build and uploaded its output without invoking the browser smoke command. The older article’s claim that every push ran 300-plus browser checks was inaccurate.

The workflow now also runs npm run check:editorial after the build. That checks the article examples’ arithmetic and generated links, metadata, feed, and sitemap. It is a static editorial check; the full browser smoke command remains separate.

A release gate needs an actual workflow step, the browser dependencies to execute it, and a nonzero exit when its assertions fail. A script on disk, an old passing log, or a checklist in prose does not supply that gate.

When recording a result, save the source or bundle hashes, browser version, viewport, flags, tested route, and time. After a source change, rerun the affected checks before carrying the verdict forward. If a check was deferred, state that plainly. This is especially important for visual projects where a newer, more ambitious scene can compile while its rendered result is still unreviewed.

Sources & revision

Read the dated source excerpt. It includes original file paths, line numbers, and SHA-256 hashes for the version used in this guide. The public repository may contain a different revision.

28 Oct 2026: Removed the false claim that every deployment runs hundreds of browser checks, corrected evaluate scoping and context isolation, and separated browser policy tests from bypass flags.

These notes use AI assistance. Read the editorial method and corrections policy. Found an error? Send a correction with this article’s title.