Documentation
¶
Overview ¶
Package-internal image comparison built on pixelmatch (the same perceptual diff library chromedp's own test suite uses). pixelmatch compares colors in YIQ space with a configurable perceptual threshold and explicitly skips anti-aliased edge pixels, so cross-Chromium text rasterization differences do not produce false positives — unlike a raw per-channel diff.
Package visualtest provides pixel-level visual regression testing for templ-components using a headless Chromium driven by chromedp.
Unlike the HTML-string golden tests in utils/golden (which normalize CSS class order and compare text), these tests render each component in a real browser, capture a screenshot, and diff pixels against a committed golden PNG. This catches regressions the string tests cannot: layout shifts, dark-mode color breakage, RTL mirroring failures, and responsive collapse.
Running ¶
Visual tests require a Chromium binary. Provide it via CHROMEDP_CHROME_PATH (set automatically by `nix run .#visual`), or they skip gracefully:
nix run .#visual # run all visual tests go test ./... # run with an explicit browser (from visualtest/) go test ./... -update # regenerate golden PNGs
The -update flag rewrites every golden with the current render, just like utils/golden. Review the diff before committing.
Why a separate module? ¶
chromedp and its transitive dependencies are heavy and test-only. Putting them in the library's go.mod would pollute every consumer's dependency graph. This package lives in its own module (./visualtest/go.mod) with a local replace directive, so `go get` of the main library never pulls them.
Index ¶
- Constants
- Variables
- func AssertScreenshot(t *testing.T, name string, component templ.Component, opts ...Options)
- func AxeViolationKey(v AxeViolation) string
- func Bool(b bool) *bool
- func FlakyFail(w http.ResponseWriter, id string) bool
- func ShutdownBrowser()
- type AxeImpact
- type AxeNode
- type AxeResults
- type AxeRuleID
- type AxeViolation
- type DemoServer
- type InteractionState
- type Options
- type Viewport
Constants ¶
const ( FlakySlowID = "e-slow" FlakyFailID = "e-fail" // FlakySlowDelay is the e2e pending window — long enough that the // optimistic pending state is deterministically observable before the // response lands, short enough not to dominate the suite. FlakySlowDelay = 1200 * time.Millisecond )
The flaky-interaction contract for optimistic-UI e2e proofs (ADR-0041).
A stateful component's test board arms two special interactions:
- FlakySlowID: the server stalls before applying the move, opening a deterministic pending window in which the optimistic register is observable (FlakySlowDelay in the e2e suite; stall-until-disconnect in the freeze-frame capture suite).
- FlakyFailID: the server always answers 500, exercising the honest revert path (card returns, pending register clears, failure flash).
Server sites share FlakyFail for the doomed branch; each site owns its stall shape. Client sites assert on cards carrying these IDs via the data-tc-kanban-card hooks.
Variables ¶
var ( ViewportMobile = Viewport{Width: viewportMobileWidth, Height: viewportMobileHeight} // iPhone SE / small phone ViewportTablet = Viewport{Width: viewportTabletWidth, Height: viewportTabletHeight} // iPad portrait ViewportDesktop = Viewport{Width: viewportDesktopWidth, Height: viewportDesktopHeight} // default laptop )
Common viewport presets matching real device CSS widths. Use them as Options{Viewport: visualtest.ViewportMobile} to test responsive breakpoints.
Functions ¶
func AssertScreenshot ¶
AssertScreenshot renders component in an isolated page, captures an element screenshot of #tc-root, and compares the pixels against the golden PNG in testdata. With -update, the golden is rewritten instead.
name uniquely identifies the golden file (e.g. "button/primary_dark"). It must not contain path separators other than a single subdirectory level.
func AxeViolationKey ¶
func AxeViolationKey(v AxeViolation) string
AxeViolationKey identifies a violation class within one route: rule ID + impact. The baseline guard compares keys and node counts per key.
func FlakyFail ¶
func FlakyFail(w http.ResponseWriter, id string) bool
FlakyFail writes the doomed interaction's 500 response and reports whether the caller must stop (id is FlakyFailID); the revert-path branch shared by every flaky-board server.
func ShutdownBrowser ¶
func ShutdownBrowser()
ShutdownBrowser closes the shared Chromium process. Called by TestMain after all tests complete.
Types ¶
type AxeImpact ¶
type AxeImpact string
AxeImpact is the axe-core severity of a violation. Only Critical and Serious block the sweep (see AxeImpact.Blocking); Moderate and Minor are recorded for trending but never gate CI.
type AxeNode ¶
type AxeNode struct {
HTML string `json:"html"`
Target []string `json:"target"`
FailureSummary string `json:"failureSummary"`
}
AxeNode is a single offending DOM node: its outer HTML, a CSS selector path, and axe's plain-language failure explanation.
type AxeResults ¶
type AxeResults struct {
Violations []AxeViolation `json:"violations"`
}
AxeResults is the subset of axe.run's result payload the sweep consumes.
func RunAxe ¶
func RunAxe(ctx context.Context) (AxeResults, error)
RunAxe injects the vendored axe-core runtime into the current page and runs a violations-only audit against the whole document. The context must be a chromedp tab context with the page already loaded.
axe.run's promise is awaited natively via chromedp's EvalAwaitPromise: the resolved value decodes by-value straight into AxeResults, and a rejected promise surfaces as a *chromedp.ExceptionError (wrapped as errAxeRuntime). No window-parked payload, no poll-sleep handshake.
func (AxeResults) BlockingViolations ¶
func (r AxeResults) BlockingViolations() []AxeViolation
BlockingViolations returns only the critical/serious violations.
type AxeViolation ¶
type AxeViolation struct {
ID AxeRuleID `json:"id"`
Impact AxeImpact `json:"impact"`
Help string `json:"help"`
HelpURL string `json:"helpUrl"`
Nodes []AxeNode `json:"nodes"`
Tags []string `json:"tags"`
}
AxeViolation is one axe-core rule failure: a rule ID, its severity, and the DOM nodes that violated it.
type DemoServer ¶
type DemoServer struct {
// Log accumulates the server's combined stdout+stderr so tests (and the
// CI smoke) can assert absence of 500 responses. Mutex-guarded: the exec
// copier goroutine writes concurrently with test reads (the data race
// TestDemoKanbanHTTPContracts caught under -race on 2026-09-17).
Log syncBuffer
// contains filtered or unexported fields
}
DemoServer is a live examples/demo process bound to an ephemeral port, built and started on demand for browser-level tests (a11y sweep, click-through flows). Tests get the real demo routes — index showcase, forms, recipes, users — not a re-implementation.
func StartDemoServer ¶
func StartDemoServer(t *testing.T) *DemoServer
StartDemoServer builds the examples/demo binary (cached by go's build cache, so the cost is paid once per machine, not per run) and starts it on an ephemeral port. Skips in -short mode. The process is killed on test cleanup.
func (*DemoServer) BaseURL ¶
func (s *DemoServer) BaseURL() string
BaseURL returns the demo server's root URL, e.g. http://127.0.0.1:41234.
func (*DemoServer) FailIfServerErrors ¶
func (s *DemoServer) FailIfServerErrors(t *testing.T)
FailIfServerErrors fails the test when the demo server log contains an HTTP 500 response line. Used by the e2e suite so a handler crash inside an otherwise-passing browser flow cannot hide.
type InteractionState ¶
type InteractionState int
InteractionState selects an interactive state to apply to #tc-root before capturing, so hover/focus styles are covered by golden screenshots.
const ( // StateRest is the default: no interaction. StateRest InteractionState = iota // StateHover moves the mouse over #tc-root, triggering :hover styles. StateHover // StateFocus focuses #tc-root, triggering :focus-visible styles. StateFocus // StateClick clicks the first interactive descendant of #tc-root (a // [popovertarget] trigger, button, or link). Use it with FullViewport // and WaitSelector to capture components whose open state renders in the // top layer — Dropdown, Popover, ContextMenu (native Popover API), Modal, // and Drawer (native <dialog>). StateClick // StateContext dispatches a contextmenu (right-click) event on the first // [data-tc-ctxmenu-trigger] (or interactive descendant) of #tc-root. This // opens components that activate on right-click, e.g. ContextMenu. StateContext )
func (InteractionState) String ¶
func (s InteractionState) String() string
String returns a human-readable name for the interaction state, used in failure messages and test output for quick diagnosis.
type Options ¶
type Options struct {
// Dark renders with the .dark class on <html> (the library's dark-mode
// strategy). nil or false renders light mode; use Bool(true) for dark.
// The pointer (not bare bool) lets callers distinguish "unset" from
// "explicitly light" in test metadata.
Dark *bool
// RTL sets dir="rtl" on <html> to test logical-property mirroring.
// nil or false renders LTR; use Bool(true) for RTL.
RTL *bool
// Viewport sets the emulated window size. Defaults to 1280x800
// (ViewportDesktop). See ViewportMobile / ViewportTablet presets.
Viewport Viewport
// MaxMismatch is the largest fraction of mismatched pixels (0–1) that
// still passes. Defaults to 0.001 (0.1%). Anti-aliasing noise is filtered
// out by pixelmatch; a real regression blows past this.
MaxMismatch float64
// Threshold is the pixelmatch perceptual color-distance threshold (0–1).
// Pixels whose YIQ distance is below it count as identical. Default 0.1;
// raise it to tolerate more rendering noise, lower it for stricter checks.
Threshold float64
// State applies an interaction (hover/focus/click) to #tc-root before capture.
State InteractionState
// FullViewport captures the full browser viewport instead of a tightly
// cropped #tc-root element. Required for components whose open state
// renders in the top layer (Popover API menus, <dialog>), because that
// content is painted outside #tc-root's bounding box and would be cropped
// by an element screenshot.
FullViewport bool
// WaitSelector, when set, is waited for (chromedp.WaitVisible) AFTER the
// interaction state is applied and BEFORE capture. Use it with StateClick
// to wait for an overlay menu to appear, e.g. WaitSelector: "[popover]".
WaitSelector string
// ClickSelector overrides the StateClick heuristic (first interactive
// descendant) with an explicit target INSIDE #tc-root, e.g.
// "[data-tc-carousel-next]". Use when the first interactive element is
// not the one that produces the state under test.
ClickSelector string
// WaitExpr, when set, is polled (every 100ms, up to 8s) after the
// interaction state and must evaluate to truthy before capture. Use for
// states that settle asynchronously without a DOM visibility change,
// e.g. a scroll-snap carousel whose smooth scroll converges:
//
// WaitExpr: `document.querySelector('[data-tc-carousel-track]').scrollLeft > 50`
WaitExpr string
}
Options configures how a component is rendered and captured.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
browser
Package browser resolves the Chromium binary for the capture tools.
|
Package browser resolves the Chromium binary for the capture tools. |
|
distserver
Package distserver serves a built website dist/ directory with Firebase cleanUrls semantics.
|
Package distserver serves a built website dist/ directory with Firebase cleanUrls semantics. |
|
tools
|
|
|
ogshot
command
Command ogshot renders the /sales social-preview card (1200×630 PNG) into website/public/og/.
|
Command ogshot renders the /sales social-preview card (1200×630 PNG) into website/public/og/. |
|
shots
command
Command shots captures full-page screenshots of a running templ-components demo server for manual visual inspection.
|
Command shots captures full-page screenshots of a running templ-components demo server for manual visual inspection. |
|
siteshots
command
Command siteshots captures full-page screenshots of the website's static dist/ output for manual visual inspection: light + dark, desktop + mobile, one fresh browser per page (a long-lived shared browser degrades across very tall captures).
|
Command siteshots captures full-page screenshots of the website's static dist/ output for manual visual inspection: light + dark, desktop + mobile, one fresh browser per page (a long-lived shared browser degrades across very tall captures). |
|
smoke
command
smoke is a tiny HTTP smoke client for the demo (and any local server): issue one request, assert the status code and body markers, exit 0/1.
|
smoke is a tiny HTTP smoke client for the demo (and any local server): issue one request, assert the status code and body markers, exit 0/1. |
|
sseload
command
sseload — M12 SSE load spike (plan μ46–μ49), Go-client stand-in for k6+xk6-sse (the extension is unavailable in this environment; see docs/testing/tooling-evaluation.md §2).
|
sseload — M12 SSE load spike (plan μ46–μ49), Go-client stand-in for k6+xk6-sse (the extension is unavailable in this environment; see docs/testing/tooling-evaluation.md §2). |