Dosu LogoDosu Logo
Ask
Join our Discord
Organization avatar
HashHivePublic
EvilBit Labs LLC
DocumentsHashHive
playwright-visual-baselines
playwright-visual-baselines
Type
External
Status
Published
Created
Jul 18, 2026
Updated
Jul 18, 2026
Source
docs/solutions/conventions/playwright-visual-baselines.md

Playwright visual baselines#

How HashHive generates, commits, and maintains Playwright screenshot baselines
(toHaveScreenshot). The first baseline is the dashboard at 1440×900
(packages/frontend/e2e/zz-dashboard.spec.ts); this convention governs every
one added after it.

Linux CI is the only canonical platform#

A screenshot baseline is a frozen PNG compared pixel-for-pixel against future
runs. Font metrics, subpixel antialiasing, and emoji rasterisation differ
between operating systems, so a baseline is only valid on the platform that
generated it.

  • Baselines are generated on a GitHub Linux runner and committed with
    Playwright's -chromium-linux platform suffix
    (e.g.
    dashboard-1440x900-chromium-linux.png).
  • Never commit a baseline generated on macOS or Windows. A local macOS run
    will report a platform/pixel mismatch — that message is expected behaviour,
    not a regression. Do not run --update-snapshots locally to silence it.

Pin the runner image — never ubuntu-latest#

The committed PNG is frozen, but ubuntu-latest is a rolling alias GitHub
migrates across major OS versions (20.04 → 22.04 → 24.04). Each migration
updates freetype, fontconfig, and the system font stack — the exact inputs the
baseline depends on (the app ships no bundled web fonts; all text rendering uses
runner-provided system fonts). An alias migration would make every baseline
self-diff on an unrelated PR with no code to blame.

  • Both the generation workflow (.github/workflows/regenerate-visual-baselines.yml)
    and the validation job (e2e-tests in .github/workflows/ci.yml) pin the
    same dated image (ubuntu-24.04). Keep them in lockstep.
  • When GitHub deprecates the pinned image, bump both jobs together and
    regenerate the baseline in the same PR.

How to generate or update a baseline (intentionally)#

Generation runs on CI because only the GitHub runner matches the validating
environment. The regenerate-visual-baselines.yml workflow does it and uploads
the PNG as an artifact — it never commits.

  1. Trigger generation.
    • On a branch / for a brand-new baseline: open the PR and add the
      regen-baselines label (a same-repo PR runs the workflow from the PR head
      branch, so this works before the workflow is on main). Applying the label
      runs the PR head's code on a runner — only label a PR (especially a fork PR)
      after reviewing its code. The workflow uses pull_request (read-only token,
      no secrets), so the blast radius is the ephemeral runner, but the label is
      an intentional opt-in, not a rubber stamp.
    • For steady-state updates once the workflow is on main: run it via
      workflow_dispatch (gh workflow run regenerate-visual-baselines.yml).
  2. Download the artifact: gh run download <run-id> -n visual-baselines
    into packages/frontend/e2e/<spec>-snapshots/. Sanity-check the PNG is
    non-trivial (> 50 KB) and carries the -chromium-linux suffix.
  3. Commit the PNG and the unskip together in one commit, so the first CI run
    of the unskipped test already has the baseline present (no red window). The
    generation workflow ephemerally unskips the test in the CI workspace only
    (--update-snapshots does not run a test.skip test) — that change is never
    committed; you commit the real unskip.
  4. Remove the regen-baselines label so generation does not re-fire on
    every subsequent push.
  5. Verify CI twice: the validation run passes (R3), and a no-op re-run also
    passes (the baseline matches itself). The re-run is the real determinism
    gate — see below.

Tolerance and determinism#

  • Tolerance is maxDiffPixelRatio: 0.02 (~2% of pixel area). Raise it only
    with written justification in the PR. Prefer stabilising or masking
    non-deterministic regions over inflating tolerance.

  • Stabilise non-deterministic state at its source, then mask the remainder.
    Masking cannot fix a conditional mount — a region that sometimes doesn't
    exist can't be masked, and its appearance reflows the layout. The dashboard
    baseline drives the page to the Live (open) connection state before
    capturing, which unmounts the conditional FreshnessLine ("Last updated X
    ago", a 1 Hz wall-clock ticker) and fixes the connection-indicator label
    width. The output[aria-label] mask is then belt-and-suspenders over residual
    status pixels. Masked regions render as magenta boxes in the committed PNG —
    expected, not corruption.

    Connection-state stabilisation approach (dashboard baseline): wait for the
    Live state
    (output[aria-label="Live"]). The e2e backend connects
    reliably, so the WebSocket reaches open within the test timeout. If a
    future surface cannot reliably connect, fall back to forcing
    ConnectionIndicator's documented status prop via a test hook.

  • Capture Motion-driven UI under prefers-reduced-motion: reduce.
    animations: 'disabled' only fast-forwards CSS animations/transitions — it
    does not freeze motion/react (JS/rAF/WAAPI) motion. emulateMedia({ reducedMotion: 'reduce' }) resolves it to a deterministic end state.

  • Scope retries: 0 to visual tests (the e2e config defaults to retries: 2 in CI) so a no-op re-run is a strict idempotency check rather than a
    3-attempts-allowed one.

Design-change updates#

When the UI changes intentionally, regenerate the baseline on Linux (steps
above) and include a before/after screenshot in the PR description so reviewers
can confirm the new baseline is the intended appearance.

Documents
.impeccable
0001-agent-enrollment-two-token-exchange
0002-docker-supervision-over-pm2
0003-bind-agent-to-enrollment-token
0004-agent-api-revisable-until-1.0
0005-version-public-api-after-1.0
0006-seaweedfs-over-minio
0007-three-api-surfaces
0008-betterauth-over-handrolled-jwt
0009-postgres-task-store-bullmq-orchestration
0010-schema-first-drizzle-zod
0011-pull-advisory-notify-task-distribution
0012-in-memory-websocket-v1-redis-deferred
0013-route-as-spec-hono-zod-openapi
0014-turborepo-bun-over-nx
0015-append-only-telemetry-timescaledb-rrd
0016-server-fanout-bus
0017-adaptive-task-sizing-lease
0018-real-db-integration-test-lane
0019-campaign-archiving-immutable-lifecycle
155-ac-traceability-matrix
155-task-distribution-assignment-spec
156-ac-traceability-matrix
156-object-storage-file-management-spec
160-login-project-selection-ui
2026-05-25-websocket-realtime-infrastructure-requirements
2026-05-29-dashboard-stats-and-read-endpoint-contract-requirements
2026-05-31-openapi-auto-generation-migration-requirements
2026-06-08-results-analysis-export-ui-requirements
2026-06-19-task-distribution-architecture
2026-06-20-campaign-archiving-requirements
2026-06-30-hash-export-import-search-requirements
227-spa-deep-link-refresh-redirect-spec
94-p0-cracker-binary-management-agent-auto-update
96-keyspace-task-distribution-spec
97-task-preemption-spec
99-attack-complexity-state-machine-spec
AGENTS
AGENTS
AI_POLICY
AI_POLICY
ARCHITECTURE
ARCHITECTURE
Agent_Authentication_&_Authorization
Agent_Heartbeat_&_Error_Handling
Agent_List_&_Detail_UI
Base Model Schema System
BullMQ_Queue_Architecture_&_Redis_Integration
CLAUDE
CONTRIBUTING
CONTRIBUTING
Campaign_Creation_Wizard_UI
Campaign_List_&_Detail_UI
Campaign_Orchestration_API
Code Quality Automation with Husky and Kiro Hooks
Core_Flows__HashHive_User_Journeys
Dashboard_&_Real-Time_Monitoring_UI
Dashboard_Stats_API_Endpoint_(GET__api_v1_dashboard_stats)
Development Workflow and Tooling Integration
ESLint and Formatting Configuration
Epic_Brief__HashHive_-_Modern_Distributed_Password_Cracking_Platform
GOTCHAS
GOTCHAS
Health Check Endpoint Enhancements
Login_&_Project_Selection_UI
MERN_proposal
Mixed Hash Type Detection & Automatic Campaign Splitting (#202)
MongoDB Connection Management
Object_Storage_&_File_Management
Project_Selection_&_User_Authentication_API
README
README
README
README
README
README
Real-Time_Events_&_WebSocket_Infrastructure
Resource_Management_API
Resource_Management_UI
Results_API_&_CSV_Export_(Dashboard_Backend)
Results_Analysis_&_Export_UI
SECURITY
SECURITY
STRATEGY
Task_Distribution_&_Assignment
Tech_Plan__HashHive_Architecture
Testing Strategy for Database and Models
advanced_task_scheduler
agent
agent-resource-protocol
agent-server-collaboration-vision
agent-token-rotation
agent_notes
agent_sync_extensions
agent_v1_contract_violations
allow_hybrid_hash_lists
attack
attack_notes
authentication_cleanup
back-compat-credential-format-migration
barrel-plus-subdir-service-split
brute_force_attack_editor
bullmq-jobid-dedup-requires-eviction
bun-test-mock-module-import-order
campaign_list_view
campaign_notes
campaigns
ci_check.prompt
code_review.prompt
contract-test-mocks-mirror-service-not-schema
copilot-instructions
core_algorithm_implementation_guide
crackable_uploads_plan
dashboard-read-endpoint-contract
dashboard-ux
default_config_suggestions
development
development
feat-agent-list-detail-ui
feat-betterauth-project-selection-rbac
feat-campaign-list-detail-ui
feat-masklist-keyspace
form-submit-payload-null-checks-2026-05-19
full_testing_architecture
getbytext-collision-waitfor-timeout
hard_password_attack_strategies
hash_hive_gap_analysis
health_status_screen
implementation_plan
learned_rules_parser_plan
llmstxt.prompt
markov_autogen_plan
new_dictionary_attack_editor
new_mask_attack_editor
openldap-testcontainer-bootstrap-race
performance_tuning.prompt
phase-1-core-infrastructure
phase-2-api-implementation
phase-2-api-implementation-part-1
phase-2-api-implementation-part-2
phase-2-api-implementation-part-3
phase-2b-resource-management
phase-2c-refactor-cleanup
phase-3-e2e-test-coverage-plan
phase-3-step-1
phase-3-step-2
phase-3-step-3
phase-3-step-4
phase-3-step-5
phase-3-web-ui-foundation
phase-4-containerization-deployment
phase-5-task-distribution
phase-6-monitoring-testing-documentation
playwright-shared-seed-user-last-project-id-flake
playwright-visual-baselines
pre-prod-wire-rename-and-schema-version-bump
previous_password_dictionary_attack_editor
projection-narrows-zrecord-wire-shape
psycopg_migration
radix-roving-focus-preventdefault-breaks-keyboard-nav
reactflow-position-by-array-index-2026-05-19
rule_explanation
salvage_templates
security_hardening.prompt
shadcn-radix-non-destructive-add-on-tailwind-v4
shared-zod-openapi-wire-contract-mirror-2026-05-25
spa_to_ssr
summary
template
test-coverage-plan
testcontainers_minio_support
testing
testing
user_flows_notes
v2-upgrade-overview
v2_agent_api_removal
work_next_task.prompt