Skip to content

Standardize screenshot consistency across OS#188

Closed
pftg wants to merge 3 commits intomasterfrom
emdash/feat-next-regression-layout-7c4
Closed

Standardize screenshot consistency across OS#188
pftg wants to merge 3 commits intomasterfrom
emdash/feat-next-regression-layout-7c4

Conversation

@pftg
Copy link
Copy Markdown
Collaborator

@pftg pftg commented Apr 14, 2026

Summary\n- add OS setup guide + ADR for deferred screenshot prep plugins\n- unify consistency config with custom CSS/JS injection\n- update GitHub setup action to install fonts and disable font hinting\n- keep README minimal and move advanced details into docs\n- harden VCS checkout against external git env overrides\n\n## Testing\n- bin/ci

Summary by Sourcery

Standardize screenshot rendering consistency across environments by adding configurable DOM normalization, font-loading waits, and cross-OS setup guidance, while hardening VCS checkout against external Git environment overrides.

New Features:

  • Add configurable DOM normalization, font-loading waits, and custom CSS/JS injection hooks before screenshot capture.
  • Introduce a one-call consistency preset and a unified configuration API for screenshot stability options.
  • Provide OS-level setup documentation and ADR for future screenshot preparation plugin architecture.

Bug Fixes:

  • Ensure VCS screenshot checkout ignores external Git environment overrides to prevent failures from user-defined Git variables.

Enhancements:

  • Extend screenshot matcher options and internal helpers to support normalization stylesheets and font readiness checks.
  • Reset new consistency-related global settings between tests for isolation.
  • Improve GitHub Actions setup to install a consistent font stack and configure font rendering for stable CI screenshots.

Documentation:

  • Document DOM normalization and consistency presets, cross-OS baseline recommendations, CI setup behavior, and OS-specific configuration for fonts and rendering.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added CSS normalization to reduce cross-OS visual rendering inconsistencies
    • Introduced font readiness detection and waiting for consistent font rendering
    • Added support for custom stylesheet and JavaScript injection
    • Introduced consistency presets (:default and :off) via enable_consistent_screenshots! and configure_consistency
  • Documentation

    • Added Architecture Decision Records index and screenshot preparation plugins documentation
    • New OS-level setup guide for standardizing fonts across Ubuntu and Alpine
    • Expanded configuration documentation with consistency options and cross-OS baseline examples
    • Updated CI integration guide with font configuration details

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 14, 2026

Reviewer's Guide

Introduces a unified screenshot consistency configuration pipeline (DOM normalization, font-loading waits, CSS/JS injection) and supporting browser helpers, tightens VCS checkout against external git env overrides, updates the GitHub Actions setup to standardize fonts and hinting, and documents OS/CI setup and consistency presets while keeping the README minimal.

Sequence diagram for preparing a consistent screenshot capture

sequenceDiagram
  actor TestSuite
  participant Screenshot
  participant ConsistencyConfig
  participant Screenshoter
  participant BrowserHelpers
  participant Browser as BrowserDOM

  TestSuite->>Screenshot: configure_consistency(preset: default) do |config|
  Screenshot->>Screenshot: enable_consistent_screenshots!(normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element)
  Screenshot->>ConsistencyConfig: new(css: custom_stylesheets, js: custom_scripts, normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element)
  Screenshot-->>ConsistencyConfig: config instance
  TestSuite->>ConsistencyConfig: mutate flags and injections
  TestSuite->>ConsistencyConfig: apply_to_screenshot!(Screenshot)
  ConsistencyConfig->>Screenshot: set normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element

  TestSuite->>Screenshoter: prepare_page_for_screenshot(timeout)
  Screenshoter->>Screenshoter: wait_images_loaded(timeout)
  alt wait_for_fonts? is true
    Screenshoter->>Screenshoter: wait_for_fonts(timeout)
    loop until fonts loaded or timeout
      Screenshoter->>BrowserHelpers: fonts_ready?()
      BrowserHelpers->>BrowserDOM: execute FONTS_READY_SCRIPT
      BrowserDOM-->>BrowserHelpers: fonts status
      BrowserHelpers-->>Screenshoter: Boolean
    end
  end
  alt normalize_css? is true
    Screenshoter->>BrowserHelpers: normalize_css(normalize_stylesheet)
    BrowserHelpers->>BrowserDOM: inject DEFAULT_NORMALIZE_CSS or custom CSS
  end
  Screenshoter->>BrowserHelpers: inject_custom_stylesheets(Screenshot.custom_stylesheets)
  BrowserHelpers->>BrowserDOM: inject style tags
  Screenshoter->>BrowserHelpers: inject_custom_scripts(Screenshot.custom_scripts)
  BrowserHelpers->>BrowserDOM: execute custom JS
  Screenshoter->>BrowserHelpers: blur_from_focused_element (if blur_active_element)
  BrowserHelpers->>BrowserDOM: blur active element
  Screenshoter-->>TestSuite: page ready for screenshot
Loading

Class diagram for unified screenshot consistency configuration

classDiagram
  class Screenshot {
    <<module>>
    +Boolean enabled
    +Boolean hide_caret
    +Boolean disable_animations
    +Boolean normalize_css
    +String normalize_stylesheet
    +Boolean wait_for_fonts
    +Array custom_stylesheets
    +Array custom_scripts
    +Numeric stability_time_limit
    +Array window_size
    +Hash capybara_screenshot_options
    +ConsistencyConfig configure_consistency(preset)
    +void enable_consistent_screenshots!(normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element)
  }

  class ConsistencyConfig {
    +Array css
    +Array js
    +Boolean normalize_css
    +Boolean wait_for_fonts
    +Boolean disable_animations
    +Boolean hide_caret
    +Boolean blur_active_element
    +initialize(css, js, normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element)
    +void apply_to_screenshot!(screenshot)
  }

  class Screenshoter {
    -Hash @capture_options
    +void prepare_page_for_screenshot(timeout)
    +void wait_images_loaded(timeout)
    +void wait_for_fonts(timeout)
    -Boolean normalize_css?()
    -String normalize_stylesheet()
    -Boolean wait_for_fonts?()
    +void save_and_process_screenshot(screenshot_path)
  }

  class BrowserHelpers {
    <<module>>
    +String DEFAULT_NORMALIZE_CSS
    +void disable_animations()
    +void normalize_css(css)
    +void inject_custom_stylesheets(stylesheets)
    +void inject_custom_scripts(scripts)
    +void inject_stylesheet(css, element_id)
    +Boolean fonts_ready?()
    +Object session
  }

  class Vcs {
    <<module>>
    +Boolean checkout_vcs(root, screenshot_path, checkout_path)
  }

  Screenshot "1" o-- "1" ConsistencyConfig : uses
  Screenshot <.. Screenshoter : config source
  Screenshoter ..> BrowserHelpers : calls
  Vcs ..> Screenshot : uses Screenshot.use_lfs
Loading

File-Level Changes

Change Details Files
Add browser helpers to normalize DOM rendering and wait for font loading before screenshots.
  • Introduce a default normalization stylesheet that disables animations, standardizes font rendering, hides carets/spinners/scrollbars, and inject it via a reusable helper
  • Add helpers to inject custom stylesheets and scripts into the page, with idempotent style tag insertion
  • Add a fonts_ready? helper that uses the document.fonts API to detect when fonts are fully loaded
lib/capybara/screenshot/diff/browser_helpers.rb
Add configurable screenshot consistency API and wire it into screenshot preparation.
  • Introduce new global configuration accessors for normalization, font waiting, and custom CSS/JS injections
  • Add ConsistencyConfig, enable_consistent_screenshots!, and configure_consistency to manage presets and propagate consistency flags onto screenshot objects
  • Extend Screenshoter#prepare_page_for_screenshot to optionally wait for fonts, apply normalization CSS, and inject custom CSS/JS in a defined order before existing blur/caret/animation behavior
  • Expose per-screenshot options for normalization and font waiting via the screenshot matcher options extraction
  • Ensure tests reset and cover new config flags and the consistency configuration behavior
lib/capybara_screenshot_diff.rb
lib/capybara/screenshot/diff/screenshoter.rb
lib/capybara/screenshot/diff/screenshot_matcher.rb
lib/capybara_screenshot_diff/dsl.rb
test/unit/screenshoter_test.rb
test/unit/screenshot_test.rb
test/test_helper.rb
Harden VCS checkout against external git environment overrides.
  • Ensure all git and git lfs invocations in the VCS helper are executed with GIT_DIR, GIT_WORK_TREE, and GIT_INDEX_FILE cleared
  • Add a unit test that forces conflicting git env vars and verifies checkout_vcs still succeeds, plus a small helper to temporarily override ENV in tests
lib/capybara/screenshot/diff/vcs.rb
test/unit/vcs_test.rb
Standardize OS/CI font setup to improve cross-OS screenshot consistency.
  • Update the shared GitHub Action to install a consistent font stack and configure fontconfig to disable hinting and subpixel adjustments, rebuilding the font cache
  • Clarify CI integration docs to describe the new font setup and link to OS-specific setup instructions
  • Add an OS setup guide for Ubuntu and Alpine documenting font installation and fontconfig configuration
.github/actions/setup-ruby-and-dependencies/action.yml
docs/ci-integration.md
docs/os-setup.md
Refine documentation to surface consistency presets and track architecture decisions.
  • Document DOM normalization, font waiting, one-line presets, and the unified configure_consistency API in the configuration guide, including a recommended cross-OS baseline setup
  • Tighten the README troubleshooting section to point users to enable_consistent_screenshots! and delegate advanced tuning to the docs
  • Introduce an ADR index and an ADR describing a deferred screenshot preparation plugin system and its trade-offs
docs/configuration.md
README.md
docs/adr/0001-screenshot-prep-plugins.md
docs/adr/README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces OS-level screenshot rendering standardization across Ubuntu (glibc) and Alpine (musl) distributions. Changes include enhanced GitHub Actions setup with font packages and Fontconfig configuration, new global configuration options for CSS normalization, font readiness detection, and custom asset injection, plus comprehensive documentation covering OS setup, CI integration, and configuration presets.

Changes

Cohort / File(s) Summary
GitHub Actions Setup
.github/actions/setup-ruby-and-dependencies/action.yml
Installs additional font packages (DejaVu, Liberation, Ubuntu, Noto color emoji) and replaces sed modification with a full Fontconfig XML configuration (/etc/fonts/local.conf) disabling hinting/autohinting and rebuilding font cache via fc-cache -f.
Documentation - Architecture & Configuration
docs/adr/README.md, docs/adr/0001-screenshot-prep-plugins.md, docs/configuration.md, docs/ci-integration.md, docs/os-setup.md
Added ADR index, deferred plugin system documentation, comprehensive configuration guide with presets (enable_consistent_screenshots!, configure_consistency), and OS-level setup instructions for font standardization across Ubuntu/Alpine with Fontconfig examples.
Documentation - Usage & FAQ
README.md
Expanded FAQ guidance for cross-OS screenshot consistency, recommending enable_consistent_screenshots! preset and pointing to docs/configuration.md for advanced tuning.
Core Configuration
lib/capybara_screenshot_diff.rb, lib/capybara_screenshot_diff/dsl.rb
Added mattr accessors (normalize_css, normalize_stylesheet, wait_for_fonts, custom_stylesheets, custom_scripts), ConsistencyConfig class for managing preset-driven settings, enable_consistent_screenshots! and configure_consistency(preset:) methods for preset application, and DSL documentation for screenshot options.
Browser Helpers - Normalization & Injection
lib/capybara/screenshot/diff/browser_helpers.rb
Introduced CSS normalization facility (DEFAULT_NORMALIZE_CSS, normalize_css()), custom stylesheet/script injection helpers (inject_custom_stylesheets, inject_custom_scripts, inject_stylesheet), and font readiness detection (FONTS_READY_SCRIPT, fonts_ready?).
Screenshot Pipeline - Options & Preparation
lib/capybara/screenshot/diff/screenshot_matcher.rb, lib/capybara/screenshot/diff/screenshoter.rb
Extended option extraction to forward normalize_css, normalize_stylesheet, wait_for_fonts flags; augmented prepare_page_for_screenshot to conditionally apply CSS normalization, inject custom assets, wait for fonts via polling loop with timeout and ExpectationNotMet error handling.
VCS Integration
lib/capybara/screenshot/diff/vcs.rb
Modified checkout_vcs to define and pass explicit git_env hash (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE set to nil) to Open3.capture3 and system calls for isolated git execution context.
Test Coverage
test/test_helper.rb, test/unit/screenshot_test.rb, test/unit/screenshoter_test.rb, test/unit/vcs_test.rb
Disabled normalization/font-waiting in test setup; added unit tests for configure_consistency presets with custom injection blocks, prepare_page_for_screenshot helper call ordering, and checkout_vcs with external git environment isolation.

Sequence Diagram(s)

sequenceDiagram
    participant Page as Browser Page
    participant SH as Screenshoter
    participant BH as BrowserHelpers
    participant Fonts as Fonts API

    SH->>BH: normalize_css(stylesheet)?
    BH->>Page: inject csdNormalizeStyle tag
    Page-->>BH: style tag injected
    
    SH->>BH: inject_custom_stylesheets(sheets)
    BH->>Page: inject csdCustomStyle tags
    Page-->>BH: styles injected
    
    SH->>BH: inject_custom_scripts(scripts)
    BH->>Page: execute scripts via session
    Page-->>BH: scripts executed
    
    SH->>BH: fonts_ready?
    BH->>Fonts: check document.fonts.status
    Fonts-->>BH: "loaded" or pending
    
    alt fonts not ready
        BH->>BH: poll with timeout
        BH->>Fonts: check status again
        Fonts-->>BH: status update
    end
    
    BH-->>SH: fonts ready or timeout error
    SH->>Page: continue (animations, caret, etc.)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • donv

🐰✨ With fonts now steady and CSS aligned,
Cross-platform screenshots are refined!
From Alpine to Ubuntu, rendering's true,
Configuration presets make testing less blue. 🎨
One screenshot, one baseline, no pixel left behind! 🐇💚

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Standardize screenshot consistency across OS' accurately reflects the main objective of the PR, which unifies screenshot consistency configuration, adds OS setup guides, updates GitHub Actions to standardize fonts, and harddens VCS checkout.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch emdash/feat-next-regression-layout-7c4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Screenshot diffs detected

Artifact Link
HTML report (inline) N/A
Full report with images N/A
All artifacts Browse all

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In Vcs.checkout_vcs, consider extracting the git_env = {"GIT_DIR" => nil, "GIT_WORK_TREE" => nil, "GIT_INDEX_FILE" => nil} hash into a small helper (e.g., clean_git_env) so the same environment setup isn’t duplicated across each Open3.capture3/system call and is easier to extend if more variables need to be scrubbed later.
  • The with_env helper in vcs_test.rb currently uses vars.transform_values { |_, _| nil }, which is slightly awkward; simplifying this to something like previous = ENV.to_h.slice(*vars.keys) would both be clearer and avoid having to manually initialize and populate previous in two passes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Vcs.checkout_vcs`, consider extracting the `git_env = {"GIT_DIR" => nil, "GIT_WORK_TREE" => nil, "GIT_INDEX_FILE" => nil}` hash into a small helper (e.g., `clean_git_env`) so the same environment setup isn’t duplicated across each `Open3.capture3`/`system` call and is easier to extend if more variables need to be scrubbed later.
- The `with_env` helper in `vcs_test.rb` currently uses `vars.transform_values { |_, _| nil }`, which is slightly awkward; simplifying this to something like `previous = ENV.to_h.slice(*vars.keys)` would both be clearer and avoid having to manually initialize and populate `previous` in two passes.

## Individual Comments

### Comment 1
<location path="test/unit/screenshoter_test.rb" line_range="51-60" />
<code_context>
+      test "#prepare_page_for_screenshot injects normalization and custom hooks in order" do
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for font-waiting behaviour, including timeout and per-call overrides

Right now, `wait_for_fonts` in `prepare_page_for_screenshot` is only covered indirectly and only for the immediate-return case. Please add targeted tests that:

1. Verify we actually call `BrowserHelpers.fonts_ready?` and exit the loop when it quickly returns `true`.
2. Cover the timeout path where `fonts_ready?` never returns `true` and we raise `CapybaraScreenshotDiff::ExpectationNotMet` with the expected message.
3. Assert that when `wait_for_fonts` is disabled via `@capture_options[:wait_for_fonts]`, we skip the font-waiting logic even if the global default is `true`.

You can stub `BrowserHelpers.fonts_ready?` and time-related calls (`Process.clock_gettime` / `sleep`) to avoid real timing dependencies and flakiness.

Suggested implementation:

```ruby
      test "#prepare_page_for_screenshot injects normalization and custom hooks in order" do
        Capybara::Screenshot.normalize_css = true
        Capybara:: Screenshot.custom_stylesheets = ["body { color: red; }"]
        Capybara::Screenshot.custom_scripts = ["window.__csd = true;"]
        Capybara::Screenshot.blur_active_element = false
        Capybara::Screenshot.hide_caret = false
        Capybara::Screenshot.disable_animations = false

        screenshoter = Screenshoter.new({wait: nil}, {driver: :chunky_png})
        calls = []

```

Because I only see part of the file, the following tests should be **added somewhere in the same test class** that currently contains `#prepare_page_for_screenshot injects normalization and custom hooks in order` (typically near the other `Screenshoter` tests). They are self‑contained and use only standard Minitest and Ruby stubbing patterns:

```ruby
      test "#prepare_page_for_screenshot waits for fonts and exits early when they become ready" do
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({}, { driver: :chunky_png })

        # Simulate fonts not ready once, then ready
        ready_sequence = [false, true]
        sleep_calls = []

        BrowserHelpers.stub(:fonts_ready?, -> { ready_sequence.shift }) do
          Kernel.stub(:sleep, ->(t) { sleep_calls << t }) do
            # Use a reasonably large timeout; we expect to exit *before* it
            assert_nil screenshoter.prepare_page_for_screenshot(timeout: 5.0)
          end
        end

        # We should have slept at least once, but should *not* have hit the timeout
        refute_empty sleep_calls
      end

      test "#prepare_page_for_screenshot times out when fonts never become ready" do
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({}, { driver: :chunky_png })

        # fonts_ready? always false
        BrowserHelpers.stub(:fonts_ready?, false) do
          # Simulate monotonic clock advancing until it passes the timeout
          times = [0.0, 0.5, 1.0, 1.5, 2.1] # last value exceeds 2.0s timeout
          Process.stub(:clock_gettime, ->(*) { times.shift || 2.1 }) do
            Kernel.stub(:sleep, ->(_t) { }) do
              error = assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) do
                screenshoter.prepare_page_for_screenshot(timeout: 2.0)
              end

              assert_match(/fonts.*not become ready within 2\.0 seconds/i, error.message)
            end
          end
        end
      end

      test "#prepare_page_for_screenshot skips font waiting when disabled per capture options" do
        # Global default is true, but this specific capture opts out
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({ wait_for_fonts: false }, { driver: :chunky_png })

        # Fail the test if fonts_ready? is ever called
        BrowserHelpers.stub(:fonts_ready?, -> { flunk("fonts_ready? should not be called when wait_for_fonts is disabled per capture") }) do
          Kernel.stub(:sleep, ->(_t) { flunk("sleep should not be called when wait_for_fonts is disabled per capture") }) do
            assert_nil screenshoter.prepare_page_for_screenshot(timeout: 2.0)
          end
        end
      end
```

If the actual API for configuring per-capture options differs (for example, if `wait_for_fonts` is passed as a keyword argument to `prepare_page_for_screenshot` instead of being in the first `Screenshoter.new` hash), adjust the third test so that it sets `@capture_options[:wait_for_fonts]` the same way production code does. The key points are:

1. The per-call override must cause `BrowserHelpers.fonts_ready?` *not* to be invoked.
2. The global `Capybara::Screenshot.wait_for_fonts` being `true` should not override that per-call setting.
3. The method should still return without error when font waiting is disabled.
</issue_to_address>

### Comment 2
<location path="test/unit/vcs_test.rb" line_range="40" />
<code_context>
           assert_equal screenshot_path.size, base_screenshot_path.size
         end
+
+        test "#checkout_vcs ignores external git env overrides" do
+          screenshot_path = file_fixture("images/a.png")
+          base_screenshot_path = Pathname.new(@base_screenshot.path)
+
+          with_env("GIT_DIR" => "/tmp/does-not-exist", "GIT_WORK_TREE" => "/tmp/does-not-exist") do
+            assert Vcs.checkout_vcs(@tmp_dir, screenshot_path, base_screenshot_path),
+              "checkout_vcs failed with overridden git env: root=#{@tmp_dir}"
+          end
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend git-env test to cover all cleared variables, especially GIT_INDEX_FILE

Since the code also clears `GIT_INDEX_FILE`, please include it in the overridden env for this test (e.g. `"GIT_INDEX_FILE" => "/tmp/does-not-exist"`). That keeps the test consistent with the implementation and ensures we detect regressions if any of these env clears are changed or removed.

```suggestion
          with_env("GIT_DIR" => "/tmp/does-not-exist", "GIT_WORK_TREE" => "/tmp/does-not-exist", "GIT_INDEX_FILE" => "/tmp/does-not-exist") do
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +51 to +60
test "#prepare_page_for_screenshot injects normalization and custom hooks in order" do
Capybara::Screenshot.normalize_css = true
Capybara::Screenshot.custom_stylesheets = ["body { color: red; }"]
Capybara::Screenshot.custom_scripts = ["window.__csd = true;"]
Capybara::Screenshot.blur_active_element = false
Capybara::Screenshot.hide_caret = false
Capybara::Screenshot.disable_animations = false

screenshoter = Screenshoter.new({wait: nil}, {driver: :chunky_png})
calls = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for font-waiting behaviour, including timeout and per-call overrides

Right now, wait_for_fonts in prepare_page_for_screenshot is only covered indirectly and only for the immediate-return case. Please add targeted tests that:

  1. Verify we actually call BrowserHelpers.fonts_ready? and exit the loop when it quickly returns true.
  2. Cover the timeout path where fonts_ready? never returns true and we raise CapybaraScreenshotDiff::ExpectationNotMet with the expected message.
  3. Assert that when wait_for_fonts is disabled via @capture_options[:wait_for_fonts], we skip the font-waiting logic even if the global default is true.

You can stub BrowserHelpers.fonts_ready? and time-related calls (Process.clock_gettime / sleep) to avoid real timing dependencies and flakiness.

Suggested implementation:

      test "#prepare_page_for_screenshot injects normalization and custom hooks in order" do
        Capybara::Screenshot.normalize_css = true
        Capybara:: Screenshot.custom_stylesheets = ["body { color: red; }"]
        Capybara::Screenshot.custom_scripts = ["window.__csd = true;"]
        Capybara::Screenshot.blur_active_element = false
        Capybara::Screenshot.hide_caret = false
        Capybara::Screenshot.disable_animations = false

        screenshoter = Screenshoter.new({wait: nil}, {driver: :chunky_png})
        calls = []

Because I only see part of the file, the following tests should be added somewhere in the same test class that currently contains #prepare_page_for_screenshot injects normalization and custom hooks in order (typically near the other Screenshoter tests). They are self‑contained and use only standard Minitest and Ruby stubbing patterns:

      test "#prepare_page_for_screenshot waits for fonts and exits early when they become ready" do
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({}, { driver: :chunky_png })

        # Simulate fonts not ready once, then ready
        ready_sequence = [false, true]
        sleep_calls = []

        BrowserHelpers.stub(:fonts_ready?, -> { ready_sequence.shift }) do
          Kernel.stub(:sleep, ->(t) { sleep_calls << t }) do
            # Use a reasonably large timeout; we expect to exit *before* it
            assert_nil screenshoter.prepare_page_for_screenshot(timeout: 5.0)
          end
        end

        # We should have slept at least once, but should *not* have hit the timeout
        refute_empty sleep_calls
      end

      test "#prepare_page_for_screenshot times out when fonts never become ready" do
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({}, { driver: :chunky_png })

        # fonts_ready? always false
        BrowserHelpers.stub(:fonts_ready?, false) do
          # Simulate monotonic clock advancing until it passes the timeout
          times = [0.0, 0.5, 1.0, 1.5, 2.1] # last value exceeds 2.0s timeout
          Process.stub(:clock_gettime, ->(*) { times.shift || 2.1 }) do
            Kernel.stub(:sleep, ->(_t) { }) do
              error = assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) do
                screenshoter.prepare_page_for_screenshot(timeout: 2.0)
              end

              assert_match(/fonts.*not become ready within 2\.0 seconds/i, error.message)
            end
          end
        end
      end

      test "#prepare_page_for_screenshot skips font waiting when disabled per capture options" do
        # Global default is true, but this specific capture opts out
        Capybara::Screenshot.wait_for_fonts = true

        screenshoter = Screenshoter.new({ wait_for_fonts: false }, { driver: :chunky_png })

        # Fail the test if fonts_ready? is ever called
        BrowserHelpers.stub(:fonts_ready?, -> { flunk("fonts_ready? should not be called when wait_for_fonts is disabled per capture") }) do
          Kernel.stub(:sleep, ->(_t) { flunk("sleep should not be called when wait_for_fonts is disabled per capture") }) do
            assert_nil screenshoter.prepare_page_for_screenshot(timeout: 2.0)
          end
        end
      end

If the actual API for configuring per-capture options differs (for example, if wait_for_fonts is passed as a keyword argument to prepare_page_for_screenshot instead of being in the first Screenshoter.new hash), adjust the third test so that it sets @capture_options[:wait_for_fonts] the same way production code does. The key points are:

  1. The per-call override must cause BrowserHelpers.fonts_ready? not to be invoked.
  2. The global Capybara::Screenshot.wait_for_fonts being true should not override that per-call setting.
  3. The method should still return without error when font waiting is disabled.

Comment thread test/unit/vcs_test.rb
screenshot_path = file_fixture("images/a.png")
base_screenshot_path = Pathname.new(@base_screenshot.path)

with_env("GIT_DIR" => "/tmp/does-not-exist", "GIT_WORK_TREE" => "/tmp/does-not-exist") do
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend git-env test to cover all cleared variables, especially GIT_INDEX_FILE

Since the code also clears GIT_INDEX_FILE, please include it in the overridden env for this test (e.g. "GIT_INDEX_FILE" => "/tmp/does-not-exist"). That keeps the test consistent with the implementation and ensures we detect regressions if any of these env clears are changed or removed.

Suggested change
with_env("GIT_DIR" => "/tmp/does-not-exist", "GIT_WORK_TREE" => "/tmp/does-not-exist") do
with_env("GIT_DIR" => "/tmp/does-not-exist", "GIT_WORK_TREE" => "/tmp/does-not-exist", "GIT_INDEX_FILE" => "/tmp/does-not-exist") do

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
test/unit/screenshoter_test.rb (1)

51-75: Missing test cleanup for global configuration state.

The test modifies global Capybara::Screenshot settings but doesn't restore them afterward, which could cause test pollution if other tests rely on default values. Consider using setup/teardown or an ensure block to reset these flags.

♻️ Proposed fix to add cleanup
       test "#prepare_page_for_screenshot injects normalization and custom hooks in order" do
+        original_normalize_css = Capybara::Screenshot.normalize_css
+        original_custom_stylesheets = Capybara::Screenshot.custom_stylesheets
+        original_custom_scripts = Capybara::Screenshot.custom_scripts
+        original_blur = Capybara::Screenshot.blur_active_element
+        original_hide_caret = Capybara::Screenshot.hide_caret
+        original_disable_animations = Capybara::Screenshot.disable_animations
+
         Capybara::Screenshot.normalize_css = true
         Capybara::Screenshot.custom_stylesheets = ["body { color: red; }"]
         Capybara::Screenshot.custom_scripts = ["window.__csd = true;"]
         Capybara::Screenshot.blur_active_element = false
         Capybara::Screenshot.hide_caret = false
         Capybara::Screenshot.disable_animations = false

         # ... test body ...

         assert_equal [
           [:normalize_css, nil],
           [:inject_custom_stylesheets, ["body { color: red; }"]],
           [:inject_custom_scripts, ["window.__csd = true;"]]
         ], calls
+      ensure
+        Capybara::Screenshot.normalize_css = original_normalize_css
+        Capybara::Screenshot.custom_stylesheets = original_custom_stylesheets
+        Capybara::Screenshot.custom_scripts = original_custom_scripts
+        Capybara::Screenshot.blur_active_element = original_blur
+        Capybara::Screenshot.hide_caret = original_hide_caret
+        Capybara::Screenshot.disable_animations = original_disable_animations
       end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/screenshoter_test.rb` around lines 51 - 75, The test
"prepare_page_for_screenshot injects normalization and custom hooks in order"
mutates global Capybara::Screenshot settings (normalize_css, custom_stylesheets,
custom_scripts, blur_active_element, hide_caret, disable_animations) without
restoring them; wrap the changes in a safe teardown (or use setup/teardown) or
an ensure block inside the test to capture original values before assignment and
restore them after calling Screenshoter#prepare_page_for_screenshot so global
state is reset for other tests.
lib/capybara/screenshot/diff/browser_helpers.rb (1)

186-195: Font readiness check relies on synchronous status property.

The current implementation checks document.fonts.status === "loaded", which works but could miss edge cases where fonts are still being loaded asynchronously. The document.fonts.ready Promise is generally more reliable for ensuring all fonts have finished loading.

That said, the polling approach in wait_for_fonts will eventually catch the "loaded" status, so this is functional. Consider this a potential future improvement if font-related flakiness is observed.

♻️ Alternative using document.fonts.ready
       FONTS_READY_SCRIPT = <<~JS
         (function() {
           if (!document.fonts) return true;
-          return document.fonts.status === "loaded";
+          // Check both status and pending load count for robustness
+          return document.fonts.status === "loaded" && document.fonts.size > 0 
+                 ? true 
+                 : document.fonts.status === "loaded";
         })();
       JS

Or for more robust async handling, consider using document.fonts.ready.then() with a callback approach in the future.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/capybara/screenshot/diff/browser_helpers.rb` around lines 186 - 195, The
fonts readiness check in FONTS_READY_SCRIPT used by BrowserHelpers.fonts_ready?
relies on the synchronous document.fonts.status value which can miss async
loads; update the script evaluated by BrowserHelpers.session.evaluate_script
(the FONTS_READY_SCRIPT constant) to use the document.fonts.ready Promise (or
await document.fonts.ready.then(() => true)) so fonts_ready? returns only after
the fonts.ready promise resolves; keep the wait_for_fonts polling logic but
replace the status check with the Promise-based readiness check for more
reliable detection.
.github/actions/setup-ruby-and-dependencies/action.yml (1)

29-34: Consider breaking long lines for readability.

Static analysis flagged lines 29 and 34 as exceeding 120 characters. While not functionally problematic, multi-line formatting would improve maintainability.

♻️ Proposed fix using YAML multi-line string
       with:
-        packages: libvips libglib2.0-0 libglib2.0-dev libwebp-dev libvips42 libpng-dev fonts-dejavu fonts-liberation fonts-ubuntu fonts-noto-color-emoji
+        packages: >-
+          libvips libglib2.0-0 libglib2.0-dev libwebp-dev libvips42 libpng-dev
+          fonts-dejavu fonts-liberation fonts-ubuntu fonts-noto-color-emoji
         version: tests-v2

     - name: Install vips (fallback)
       if: ${{ inputs.cache-apt-packages != 'true' }}
-      run: sudo apt-get -qq update && sudo apt-get -qq install -y libvips fonts-dejavu fonts-liberation fonts-ubuntu fonts-noto-color-emoji
+      run: |
+        sudo apt-get -qq update
+        sudo apt-get -qq install -y libvips \
+          fonts-dejavu fonts-liberation fonts-ubuntu fonts-noto-color-emoji
       shell: bash
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/setup-ruby-and-dependencies/action.yml around lines 29 - 34,
The long YAML values exceed 120 characters; split the long "packages" value and
the long "run" command in the "Install vips (fallback)" step into multi-line
form for readability — for example, change the "packages:" entry to a YAML
sequence or use a folded block (>) to wrap the long list, and replace the
single-line "run: sudo apt-get -qq update && sudo apt-get -qq install -y libvips
..." with a multi-line folded string or multiple run lines in the "Install vips
(fallback)" step so each line stays under 120 chars and the identifiers
(packages, Install vips (fallback), run) remain unchanged.
test/unit/screenshot_test.rb (1)

28-45: Consider restoring boolean configuration flags after test.

While the test correctly resets custom_stylesheets and custom_scripts at the start, it modifies other global flags (normalize_css, wait_for_fonts, disable_animations, hide_caret, blur_active_element) without restoring them. This could affect subsequent tests that rely on default values.

♻️ Proposed fix with ensure block
     test "configure_consistency presets defaults and allows custom injections" do
+      # Store original values
+      orig_normalize_css = Capybara::Screenshot.normalize_css
+      orig_wait_for_fonts = Capybara::Screenshot.wait_for_fonts
+      orig_disable_animations = Capybara::Screenshot.disable_animations
+      orig_hide_caret = Capybara::Screenshot.hide_caret
+      orig_blur_active_element = Capybara::Screenshot.blur_active_element
+      orig_custom_stylesheets = Capybara::Screenshot.custom_stylesheets
+      orig_custom_scripts = Capybara::Screenshot.custom_scripts
+
       Capybara::Screenshot.custom_stylesheets = []
       Capybara::Screenshot.custom_scripts = []

       # ... rest of test ...

       assert_includes Capybara::Screenshot.custom_scripts, "window.__csd_test = true;"
+    ensure
+      Capybara::Screenshot.normalize_css = orig_normalize_css
+      Capybara::Screenshot.wait_for_fonts = orig_wait_for_fonts
+      Capybara::Screenshot.disable_animations = orig_disable_animations
+      Capybara::Screenshot.hide_caret = orig_hide_caret
+      Capybara::Screenshot.blur_active_element = orig_blur_active_element
+      Capybara::Screenshot.custom_stylesheets = orig_custom_stylesheets
+      Capybara::Screenshot.custom_scripts = orig_custom_scripts
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/screenshot_test.rb` around lines 28 - 45, The test
"configure_consistency presets defaults and allows custom injections" mutates
global flags on Capybara::Screenshot and doesn't restore them; before calling
Capybara::Screenshot.configure_consistency, capture the original values of
Capybara::Screenshot.normalize_css, .wait_for_fonts, .disable_animations,
.hide_caret, and .blur_active_element, and ensure they are reset in an
ensure/finally block (or use Minitest's teardown) after the assertions so global
state is restored; keep the existing resets for custom_stylesheets and
custom_scripts and only wrap the flag changes around the configure_consistency
call and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/capybara_screenshot_diff.rb`:
- Around line 71-77: apply_to_screenshot! currently copies flags like
normalize_css but never persists any config.css or config.js assignments made in
configure_consistency blocks, so assignment-style overrides are lost; update
apply_to_screenshot! (and the same logic around configure_consistency usage) to
also write any configured css and js back onto the screenshot (e.g., if
config.css or config.js are non-nil/non-empty, set screenshot.css = config.css
and screenshot.js = config.js) so both mutation and full-assignment overrides
are honored; reference apply_to_screenshot!, configure_consistency, config.css
and config.js when making the change.

In `@lib/capybara/screenshot/diff/vcs.rb`:
- Around line 23-24: The call to system(git_env, "git", "-C", root_path, "lfs",
"smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL) inside
checkout_vcs currently ignores the exit status, allowing checkout_vcs to return
true even when git lfs smudge fails; capture the return value (e.g. result =
system(...)) and if it is false or nil propagate the failure by marking success
as false or returning false immediately (and optionally log the error),
referencing the existing variables and method names (checkout_vcs, git_env,
root_path, tmp_path, checkout_path) so the checkout failure is not silently
ignored.

---

Nitpick comments:
In @.github/actions/setup-ruby-and-dependencies/action.yml:
- Around line 29-34: The long YAML values exceed 120 characters; split the long
"packages" value and the long "run" command in the "Install vips (fallback)"
step into multi-line form for readability — for example, change the "packages:"
entry to a YAML sequence or use a folded block (>) to wrap the long list, and
replace the single-line "run: sudo apt-get -qq update && sudo apt-get -qq
install -y libvips ..." with a multi-line folded string or multiple run lines in
the "Install vips (fallback)" step so each line stays under 120 chars and the
identifiers (packages, Install vips (fallback), run) remain unchanged.

In `@lib/capybara/screenshot/diff/browser_helpers.rb`:
- Around line 186-195: The fonts readiness check in FONTS_READY_SCRIPT used by
BrowserHelpers.fonts_ready? relies on the synchronous document.fonts.status
value which can miss async loads; update the script evaluated by
BrowserHelpers.session.evaluate_script (the FONTS_READY_SCRIPT constant) to use
the document.fonts.ready Promise (or await document.fonts.ready.then(() =>
true)) so fonts_ready? returns only after the fonts.ready promise resolves; keep
the wait_for_fonts polling logic but replace the status check with the
Promise-based readiness check for more reliable detection.

In `@test/unit/screenshot_test.rb`:
- Around line 28-45: The test "configure_consistency presets defaults and allows
custom injections" mutates global flags on Capybara::Screenshot and doesn't
restore them; before calling Capybara::Screenshot.configure_consistency, capture
the original values of Capybara::Screenshot.normalize_css, .wait_for_fonts,
.disable_animations, .hide_caret, and .blur_active_element, and ensure they are
reset in an ensure/finally block (or use Minitest's teardown) after the
assertions so global state is restored; keep the existing resets for
custom_stylesheets and custom_scripts and only wrap the flag changes around the
configure_consistency call and assertions.

In `@test/unit/screenshoter_test.rb`:
- Around line 51-75: The test "prepare_page_for_screenshot injects normalization
and custom hooks in order" mutates global Capybara::Screenshot settings
(normalize_css, custom_stylesheets, custom_scripts, blur_active_element,
hide_caret, disable_animations) without restoring them; wrap the changes in a
safe teardown (or use setup/teardown) or an ensure block inside the test to
capture original values before assignment and restore them after calling
Screenshoter#prepare_page_for_screenshot so global state is reset for other
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e40b3e7-e8e3-4f63-a10b-ab3f24609478

📥 Commits

Reviewing files that changed from the base of the PR and between 64c5aef and e1fe15e.

📒 Files selected for processing (17)
  • .github/actions/setup-ruby-and-dependencies/action.yml
  • README.md
  • docs/adr/0001-screenshot-prep-plugins.md
  • docs/adr/README.md
  • docs/ci-integration.md
  • docs/configuration.md
  • docs/os-setup.md
  • lib/capybara/screenshot/diff/browser_helpers.rb
  • lib/capybara/screenshot/diff/screenshot_matcher.rb
  • lib/capybara/screenshot/diff/screenshoter.rb
  • lib/capybara/screenshot/diff/vcs.rb
  • lib/capybara_screenshot_diff.rb
  • lib/capybara_screenshot_diff/dsl.rb
  • test/test_helper.rb
  • test/unit/screenshot_test.rb
  • test/unit/screenshoter_test.rb
  • test/unit/vcs_test.rb

Comment on lines +71 to +77
def apply_to_screenshot!(screenshot)
screenshot.normalize_css = normalize_css
screenshot.wait_for_fonts = wait_for_fonts
screenshot.disable_animations = disable_animations
screenshot.hide_caret = hide_caret
screenshot.blur_active_element = blur_active_element
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist css/js assignments from configure_consistency blocks.

At Line 119, callers can mutate or assign config.css / config.js. But apply_to_screenshot! (Lines 71-77) never writes those fields back, so assignment-style overrides are dropped silently.

💡 Proposed fix
        def apply_to_screenshot!(screenshot)
+          screenshot.custom_stylesheets = Array(css)
+          screenshot.custom_scripts = Array(js)
           screenshot.normalize_css = normalize_css
           screenshot.wait_for_fonts = wait_for_fonts
           screenshot.disable_animations = disable_animations
           screenshot.hide_caret = hide_caret
           screenshot.blur_active_element = blur_active_element
        end

Also applies to: 110-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/capybara_screenshot_diff.rb` around lines 71 - 77, apply_to_screenshot!
currently copies flags like normalize_css but never persists any config.css or
config.js assignments made in configure_consistency blocks, so assignment-style
overrides are lost; update apply_to_screenshot! (and the same logic around
configure_consistency usage) to also write any configured css and js back onto
the screenshot (e.g., if config.css or config.js are non-nil/non-empty, set
screenshot.css = config.css and screenshot.js = config.js) so both mutation and
full-assignment overrides are honored; reference apply_to_screenshot!,
configure_consistency, config.css and config.js when making the change.

Comment on lines +23 to 24
system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate git lfs smudge failures to success.

On Line 23, the git lfs smudge exit status is ignored. If smudge fails, checkout_vcs can still return true, which masks checkout errors.

🔧 Proposed fix
             if success
-              system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
+              success = system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
             end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
end
success = system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/capybara/screenshot/diff/vcs.rb` around lines 23 - 24, The call to
system(git_env, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out:
checkout_path.to_s, err: File::NULL) inside checkout_vcs currently ignores the
exit status, allowing checkout_vcs to return true even when git lfs smudge
fails; capture the return value (e.g. result = system(...)) and if it is false
or nil propagate the failure by marking success as false or returning false
immediately (and optionally log the error), referencing the existing variables
and method names (checkout_vcs, git_env, root_path, tmp_path, checkout_path) so
the checkout failure is not silently ignored.

@pftg pftg closed this Apr 14, 2026
@pftg pftg deleted the emdash/feat-next-regression-layout-7c4 branch April 14, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant