diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e511d1a..6e33905d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -330,6 +330,12 @@ One removal 2.0 cannot warn about, written down instead: the legacy `LOADED_DRIV content and version, matching this repository. `capybara-screenshot-diff` remains the name to install; the mirror reserves the forward-looking one. **Install one, never both.** +- **Reporters can list the screenshots that were not compared.** + `SnapDiff::Reporting.uncompared` returns the ones behind the summary line's "new" and + "re-recorded" counts, complete by the time reporters `finalize` (fork-parallel workers + included): each with its `name`, the captured file's `path`, and the `reason` — `:new` + (no committed baseline) or `:rerecorded` (`record: :all`). The bundled HTML report lists + new screenshots from it - New documentation shipped inside the gem: [docs/snapdiff.md](docs/snapdiff.md) (canonical API) and [docs/UPGRADING.md](docs/UPGRADING.md) (every renamed constant, which names warn, @@ -416,10 +422,23 @@ One removal 2.0 cannot warn about, written down instead: the legacy `LOADED_DRIV - **A size change labels its images like every other failure.** `Dimensions have changed` now lists `baseline:` and `actual:`, relative to the project root, instead of two bare absolute paths, and never names annotations left over from an earlier run +- **The HTML report lists new screenshots.** A screenshot captured with no committed + baseline appears after the failures, marked `new`, with its capture and the + `git add` that makes it the baseline; a run whose only finding is a new screenshot + now writes a report. The report's `new` count is the summary line's, including + across `parallelize(workers: N)`. Re-recorded screenshots (`record: :all`) are + counted in the report but not yet listed +- **Under `record: :none` a new screenshot is counted.** The run still fails as before; + the summary line now says `1 new` instead of `0 new` +- **A committed baseline taken by a same-name test is reported as disappeared, in every + mode.** Before, if it vanished right after the checkout, the run also warned there was + no committed baseline and counted it as new; under `record: :none` it failed with + "No existing screenshot found … `git add` it" instead of saying it disappeared ### Known limitations - **One process per worker** (`parallel_tests`, RSpec, CI sharding) still writes one - report per process to the same path, so only the last to finish survives. Rails' + report per process to the same path, so only the last to finish survives, + now including a process whose only finding is a new screenshot. Rails' `parallelize(workers: N)` and `parallelize(with: :threads)` are both complete — see [docs/reporters.md](docs/reporters.md#parallel-test-runs) diff --git a/README.md b/README.md index 162d8e70..3c5e6a71 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ Add one line to get an interactive dashboard for reviewing all screenshot differ ```ruby # test/test_helper.rb -require 'capybara_screenshot_diff/reporters/html' +require 'snap_diff/reporters/html' ``` After tests run, open `doc/screenshots/snap_diff_report.html`: diff --git a/docs/ci-integration.md b/docs/ci-integration.md index 873a0137..905e9f16 100644 --- a/docs/ci-integration.md +++ b/docs/ci-integration.md @@ -77,6 +77,11 @@ That's it. On failure, this will: - Post a PR comment with links to the inline report and full artifact download - Add a job summary with report links (visible in the Actions UI) +The report also lists *new* screenshots — captured with no committed baseline — each with the +`git add` that makes it the baseline. On CI a new screenshot fails its test, so `if: failure()` +uploads it. If you set `SnapDiff.config.record = :once` on CI, a run whose only news is a new +screenshot passes: upload with `if: always()` to still get the report. + #### Inputs | Input | Default | Description | diff --git a/docs/reporters.md b/docs/reporters.md index 6b8b0cdb..1452aada 100644 --- a/docs/reporters.md +++ b/docs/reporters.md @@ -10,7 +10,7 @@ require 'snap_diff/reporters/html' # canonical # require 'capybara_screenshot_diff/reporters/html' # legacy, same thing ``` -After running tests, open the report (generated only when there are failures): +After running tests, open the report (written when a screenshot changed or is new): ```bash open doc/screenshots/snap_diff_report.html @@ -18,7 +18,11 @@ open doc/screenshots/snap_diff_report.html The report includes a sidebar with thumbnails, side-by-side comparison with diff toggle, search, and summary stats. No configuration needed — just require it. -**Note:** The report is not generated when all screenshots match. +New screenshots — captured with no committed baseline — are listed after the failures, marked +`new`: the capture, and the `git add` that makes it the baseline (baselines are read from git, so +committing it is what accepts it). Their count in the report is the summary line's `new`. + +**Note:** The report is not written when every screenshot matched and none is new. ## The end-of-run summary @@ -30,8 +34,9 @@ Every run ends with what it actually did, whether or not you require a reporter: - **verified** — a committed baseline existed and was compared - **changed** — of those, the ones that differed -- **new** — captured but *not* compared, for want of a committed baseline: neither a pass nor a - failure. Commit the files to turn them into baselines. +- **new** — captured but *not* compared, for want of a committed baseline: not a pass. Under + `record: :none` (the default on CI) it also fails its test. Commit the files to turn them into + baselines. `0 verified` is printed as `NOTHING WAS VERIFIED`, because it is the only signal for the failures no per-assertion rule can see: a `rake test` that ran zero system tests, or an inherited `GIT_DIR` @@ -52,8 +57,8 @@ that hook fires in the process holding the results depends on how your runner pa | How the suite runs | Report | | --- | --- | | Serial | Written, complete. | -| `parallelize(with: :threads)` (also the default on JRuby) | Written, complete — same failures and counts as a serial run; only the order of the entries differs. Verified over repeated runs, provided [screenshot names are unique across tests](thread_safety.md#screenshot-names-must-be-unique-across-tests). | -| `parallelize(workers: N)` (Rails' default, forks) | Written, complete — one report at the usual path, merged from every worker. No configuration needed. | +| `parallelize(with: :threads)` (also the default on JRuby) | Written, complete — same failures, new screenshots and counts as a serial run; only the order of the entries differs. Verified over repeated runs, provided [screenshot names are unique across tests](thread_safety.md#screenshot-names-must-be-unique-across-tests). | +| `parallelize(workers: N)` (Rails' default, forks) | Written, complete — one report at the usual path, merged from every worker, new screenshots included. No configuration needed. | | One process per worker (`parallel_tests`, RSpec, CI sharding) | Written, but only the **last** process to finish is in it; the others are overwritten. | Under Rails' forking parallelism the workers hold the results but never finalize: Minitest skips its @@ -125,6 +130,12 @@ the framework's end-of-suite hook (`Minitest.after_run`, RSpec `after(:suite)`, finalized and then warned about. A reporter that raises is warned about and skipped — the others still run. +`record` receives every assertion, but only a compared one carries a `compare`; the rest have +`compare: nil` and no path or reason of their own. Those — each new (`reason: :new`) or +re-recorded (`reason: :rerecorded`) screenshot, with its `name` and captured `path` — are in +`SnapDiff::Reporting.uncompared`, complete by the time `finalize` runs (fork-parallel workers +included). The HTML report lists new screenshots from it. + Full details in [Custom reporters](snapdiff.md#custom-reporters). ## AI triage reporter diff --git a/docs/snapdiff.md b/docs/snapdiff.md index b96cae24..e27bd61f 100644 --- a/docs/snapdiff.md +++ b/docs/snapdiff.md @@ -258,6 +258,19 @@ and then warned about (`[snap_diff] Reporter … failed (NoMethodError: undefine A reporter that raises is warned about and skipped — the other reporters still run. +`record` receives every assertion, but a screenshot that was not compared arrives with +`compare: nil` and nothing else. Those — the summary line's "new" and "re-recorded" — are in +`SnapDiff::Reporting.uncompared`, complete by the time `finalize` runs (fork-parallel workers +included): + +```ruby + def finalize + new_ones = SnapDiff::Reporting.uncompared.select { |s| s.reason == :new } + # each has #name, #path (the captured file as a Pathname, or nil when it isn't known) + # and #reason (:new or :rerecorded); check s.path&.exist? before reading it + end +``` + The bundled HTML report is just a pre-registered reporter of this kind: ```ruby diff --git a/lib/snap_diff/reporters/html.rb b/lib/snap_diff/reporters/html.rb index dda8cb45..d2b990e3 100644 --- a/lib/snap_diff/reporters/html.rb +++ b/lib/snap_diff/reporters/html.rb @@ -5,6 +5,7 @@ require "fileutils" require "pathname" require "json" +require "shellwords" # The auto-registration block at the bottom of this file registers with # SnapDiff::Reporting. require "snap_diff/reporting" @@ -59,7 +60,7 @@ def record(assertions) def finalize @mutex.synchronize do return if @finalized - return if failures.empty? + return if failures.empty? && new_count.zero? write_report @finalized = true @@ -89,6 +90,11 @@ def merge_state!(state) def passed = total - failures.size def failed = failures.size + # Screenshots captured but not compared: new ones are listed, + # re-recorded ones only counted. + def new_screenshots = SnapDiff::Reporting.uncompared.select { |screenshot| screenshot.reason == :new } + def new_count = new_screenshots.size + def rerecorded_count = SnapDiff::Reporting.uncompared.size - new_count # Both customer personas named this path as the best output in the # product, so it gets its own line -- but only when a report was @@ -106,6 +112,7 @@ def summary # Attach available contributions and return the rendered HTML report. def render attach_annotations + @entries = failures + new_screenshot_entries ERB.new(File.read(self.class.template_path)).result(binding) end @@ -164,6 +171,28 @@ def attach_annotations end end + # Built at render time: the image and the `git add` are offered only + # for a file that is on disk now (#260). + def new_screenshot_entries + new_screenshots.map { |screenshot| new_screenshot_entry(screenshot.name, screenshot.path) } + end + + def new_screenshot_entry(name, path) + entry = {name: name, new_screenshot: true} + image = resolve_image(path) + return entry unless image + + entry.merge(new: image, git_add: "git add -- #{Shellwords.escape(display_path(path))}") + end + + # Relative to the project root, where the user runs `git add` -- the + # same form as the failure messages. Absolute when the file is outside + # the root, where a relative path would mislead. + def display_path(path) + relative = Pathname(path).relative_path_from(SnapDiff.config.root).to_path + relative.start_with?("..") ? path.to_s : relative + end + def resolve_image(path) return unless path diff --git a/lib/snap_diff/reporters/templates/report.html.erb b/lib/snap_diff/reporters/templates/report.html.erb index 00707524..b91a059d 100644 --- a/lib/snap_diff/reporters/templates/report.html.erb +++ b/lib/snap_diff/reporters/templates/report.html.erb @@ -3,7 +3,7 @@ - SnapDiff Report — <%= failed %> failures + SnapDiff Report — <%= failed %> failures<%= ", #{new_count} new" if new_count.positive? %> @@ -170,7 +177,7 @@
@@ -180,6 +187,10 @@ <%= failed %> failed <%= passed %> passed <%= total %> total + <%= new_count %> new +<% if rerecorded_count.positive? %> + <%= rerecorded_count %> re-recorded +<% end %> @@ -233,7 +244,7 @@