visualtest

package module
v0.0.0-...-285ce58 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 28 Imported by: 0

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

View Source
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

View Source
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

func AssertScreenshot(t *testing.T, name string, component templ.Component, opts ...Options)

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 Bool

func Bool(b bool) *bool

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.

const (
	AxeImpactCritical AxeImpact = "critical"
	AxeImpactSerious  AxeImpact = "serious"
	AxeImpactModerate AxeImpact = "moderate"
	AxeImpactMinor    AxeImpact = "minor"
)

func (AxeImpact) Blocking

func (i AxeImpact) Blocking() bool

Blocking reports whether the impact gates the sweep: the zero-critical guard fails on any NEW critical or serious violation.

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 AxeRuleID

type AxeRuleID string

AxeRuleID identifies an axe-core rule (e.g. "image-alt").

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.

type Viewport

type Viewport struct {
	Width  int
	Height int
}

Viewport sets the browser window size for a screenshot.

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL