Current section

Files

Jump to
excessibility lib mix tasks excessibility_compare.ex
Raw

lib/mix/tasks/excessibility_compare.ex

defmodule Mix.Tasks.Excessibility.Compare do
@shortdoc "Compares snapshots against baseline and resolves diffs"
@moduledoc """
Compares current snapshots against the baseline and prompts for resolution.
For each snapshot that differs from its baseline, this task:
1. Creates `.good.html` (baseline) and `.bad.html` (new) files
2. Opens both in your browser for comparison
3. Prompts you to choose which version to keep
4. Updates the baseline with your choice
## Usage
# Interactive mode - prompts for each diff
$ mix excessibility.compare
# Keep all baseline (good) versions
$ mix excessibility.compare --keep good
# Accept all new (bad) versions as baseline
$ mix excessibility.compare --keep bad
## Options
- `--keep good` - Automatically keep all baseline versions (no changes)
- `--keep bad` - Automatically accept all new versions as baseline
## Workflow
1. Run your tests to generate snapshots
2. Run this task to compare against baseline
3. Review diffs and choose which to keep
4. The selected version becomes the new baseline
"""
use Mix.Task
@requirements ["app.config"]
@impl true
def run(args) do
{opts, _argv, _invalid} =
OptionParser.parse(args,
strict: [keep: :string]
)
keep =
case opts[:keep] do
nil -> nil
"good" -> :good
"bad" -> :bad
other -> raise ArgumentError, "Unknown --keep value #{inspect(other)}. Use good or bad."
end
ensure_directories!()
snapshots =
snapshots_dir()
|> Path.join("*.html")
|> Path.wildcard()
|> Enum.reject(&String.ends_with?(&1, [".bad.html", ".good.html"]))
if snapshots == [] do
Mix.shell().info("No snapshots found in #{snapshots_dir()}")
exit({:shutdown, 0})
end
diffs = Enum.filter(snapshots, &differs_from_baseline?/1)
if diffs == [] do
Mix.shell().info("All #{length(snapshots)} snapshot(s) match baseline.")
else
Mix.shell().info("Found #{length(diffs)} diff(s) out of #{length(snapshots)} snapshot(s).\n")
Enum.each(diffs, &resolve_diff(&1, keep))
cleanup_diff_files()
end
end
defp differs_from_baseline?(snapshot_path) do
filename = Path.basename(snapshot_path)
baseline_path = Path.join(baseline_dir(), filename)
case File.read(baseline_path) do
{:ok, baseline_html} ->
{:ok, snapshot_html} = File.read(snapshot_path)
baseline_html != snapshot_html
{:error, :enoent} ->
Mix.shell().info("No baseline for #{filename} - run `mix excessibility.baseline` first")
false
end
end
defp resolve_diff(snapshot_path, keep) do
filename = Path.basename(snapshot_path)
baseline_path = Path.join(baseline_dir(), filename)
{:ok, new_html} = File.read(snapshot_path)
{:ok, old_html} = File.read(baseline_path)
bad_path = String.replace(snapshot_path, ".html", ".bad.html")
good_path = String.replace(snapshot_path, ".html", ".good.html")
File.write!(bad_path, new_html)
File.write!(good_path, old_html)
choice =
keep ||
prompt_choice(filename, good_path, bad_path)
case choice do
:good ->
Mix.shell().info("Kept baseline for #{filename}")
:bad ->
File.write!(baseline_path, new_html)
Mix.shell().info("Updated baseline for #{filename}")
end
end
defp prompt_choice(filename, good_path, bad_path) do
system_mod = Application.get_env(:excessibility, :system_mod, Excessibility.System)
system_mod.open_with_system_cmd(good_path)
system_mod.open_with_system_cmd(bad_path)
"""
Diff: #{filename}
(g)ood = baseline, (b)ad = new version
Keep which? [g/b]:
"""
|> IO.gets()
|> parse_choice()
end
defp parse_choice(input) do
input
|> case do
nil -> "b"
str -> String.trim(String.downcase(str))
end
|> case do
choice when choice in ["g", "good"] -> :good
choice when choice in ["b", "bad"] -> :bad
_ ->
Mix.shell().info("Unrecognized response, defaulting to bad (new version).")
:bad
end
end
defp cleanup_diff_files do
snapshots_dir()
|> Path.join("*.{good,bad}.html")
|> Path.wildcard()
|> Enum.each(&File.rm/1)
end
defp ensure_directories! do
File.mkdir_p!(snapshots_dir())
File.mkdir_p!(baseline_dir())
end
defp snapshots_dir do
Path.join(output_path(), "html_snapshots")
end
defp baseline_dir do
Path.join(output_path(), "baseline")
end
defp output_path do
Application.get_env(:excessibility, :excessibility_output_path, "test/excessibility")
end
end