component_visual_test generates Playwright Test captures from consumer .visual.tsx modules against
caller-owned browser runtime in isolated Linux action by default, or on
Linux amd64 host with host_vrt = True. It provides comparison,
failure artifacts, and explicit <name>.update target. Runtime consumed
through Bazel; separate npm publication not required. See
architecture and visual testing design
for rationale. Start with user guide for dependency wiring
and API reference for all supported attributes.
Follow preset setup, then:
cd examples/react
.web-e2e/run test //:visual_test
.web-e2e/run run //:visual_test.updateBazel installs locked npm dependencies. Update command replaces this example’s PNG baselines only after every test succeeds. Review image diff before committing. Each VRT target must own separate baseline directory.
Follow getting started for dependency wiring and built
input targets. Register .visual.tsx modules with installVisualGallery in
your gallery entry point, then build it as directory of static assets.
load("@rules_web_e2e//component:defs.bzl", "browser_shell")
load("@rules_web_e2e//vrt:defs.bzl", "component_visual_test")
browser_shell(name = "gallery", assets = ":built_gallery", entry_point = "gallery.html")
component_visual_test(
name = "visual_test",
browser = ":linux_browser",
shell = ":gallery",
matching = ":matching",
baselines = glob(["__screenshots__/*.png"], allow_empty = True),
)Consumer build owns strict typechecking, transpilation, providers, CSS, fonts, and generated assets. Runner does not compile application. See the complete example.
matching accepts dictionary of JSON numeric strings or compiled module
exporting VisualMatching, just like page VRT: configure per-pixel
threshold and either maxDiffPixels or maxDiffPixelRatio. Defaults use
Playwright's pixelmatch comparator with threshold 0.1 and zero mismatched pixels.
Viewport, language, theme, density, and capture hooks remain visual options.
Gallery waits for loaded fonts; hooks should wait for application readiness.
Custom compiled server or action-local URL can replace shell; see
customization. Most consumers need no Playwright config.
- See repository pins. Actiond VRT supports x64/ARM64; local Linux supports both; host VRT supports amd64. Cross-architecture pixel equivalence remains unverified.
- Declared runtime supplies Chromium, Node, libraries, and fonts. Pin it and keep its browser compatible with consumer's Playwright packages.
- Fixture server and browser share action-local loopback network. External
services and inherited environment unsupported; declare fixtures and
env. - Linux amd64 and ARM64 comparison use native Bazel tests. Test retries,
--runs_per_test,--nocache_test_results, and Bazel's test timeout apply to browser execution. Screenshots and JUnit ordinary test outputs. - Capture remains cacheable remote action; only
.updateapplies successful captures to source baselines. Withhost_vrt = True, comparison and capture run locally. Capture actions still useexecution_timeout_secondsbecause Bazel test timeouts do not bound build actions. - Built inputs and harnesses immutable declared files; home/cache directories and browser outputs use private scratch space. See isolation.
- Compare mode reads declared baselines directly from runfiles. Missing or
changed screenshots fail without modifying source baselines. Playwright writes
JUnit and image attachments under
TEST_UNDECLARED_OUTPUTS_DIR. - Update mode captures into a fresh directory, then replaces the owned PNG set.
Source baseline directories must contain only regular PNGs. It rejects
empty captures, path traversal, directory symlinks, and CLI filters. It also
rejects baseline edits made during capture and concurrent updates to same
directory. Interrupted update may leave
.vrt-update.lockdirectory beside baselines; inspect baselines before removing that stale lock. - Bazel's
timeoutbounds native comparison.execution_timeout_secondsbounds capture/standalone invocations (default 180 seconds per phase); managed server startup has a separate 30-second deadline. Failed updates retain baselines and report their artifacts.
Use vrt: {capture: 'viewport'} when content fixed-position or rendered in
portal outside normal layout flow:
{
visualId: 'dialog',
name: 'Dialog',
render: () => <Dialog open />,
vrt: {capture: 'viewport', viewport: {width: 390, height: 844}},
}Default capture: 'element' screenshots getScreenshotElement() (or #root)
using its element bounds. Selecting document.body does not mean viewport capture:
fixed descendants may leave body's layout height at zero.
Viewport capture runs beforeCapture and waits for fonts, skips screenshot-element
selection, and uses Playwright's page screenshot assertion. It captures visible
viewport, not full scrollable page. viewport configures browser dimensions
in either mode; deviceScaleFactor and config's screenshot scale retain their
usual behavior. For explicit full-page screenshots use native visual_test spec.
For hover states, set vrt.hoverSelector to CSS selector for visible
target. Runner moves real browser pointer after mounting visual and
before calling beforeCapture, so hook can assert :hover styles.
Capture writes PNGs straight into the Bazel result at artifacts/reference/.
Partial PNGs survive test failure or a stalled runner. The guarded Bazel writer
applies them to source only after the full capture succeeds.
Pass capture_manifest = ":gallery-captures.json" to use a declared JSON catalog
instead of launching a discovery browser. The file may be a build output.
It contains the array returned by visualCaptures(modules) from the same visual
metadata used by the gallery. Generate both from one source when possible.
[{"id":"Counter/default","name":"Counter / Default","screenshotName":"counter.png"}]The catalog participates in Bazel's input hash. Each worker checks it against the loaded gallery before taking screenshots; missing stories and stale options fail. Omit it only for galleries that need browser execution to discover their catalog.
For Windows high contrast, set vrt.forcedColors: 'active'. The runner emulates
the forced-colors media feature on that capture's page before loading the
gallery, so other visuals in the same target are unaffected.