Current section

Files

Jump to
excessibility lib snapshot.ex
Raw

lib/snapshot.ex

defmodule Excessibility.Snapshot do
@moduledoc """
Handles snapshot generation, file writing, naming, diffing, and screenshots.
"""
alias Excessibility.HTML
require Logger
@output_path Application.compile_env(
:excessibility,
:excessibility_output_path,
"test/excessibility"
)
@snapshots_path Path.join(@output_path, "html_snapshots")
@baseline_path Path.join(@output_path, "baseline")
@doc """
Produces a snapshot from a supported source.
Options:
* `:open_browser?` - open the snapshot in a browser (default: false)
* `:cleanup?` - deletes existing snapshots for the current test module (default: false)
* `:name` - custom filename (overrides default module/line-based name)
* `:tag_on_diff` - if true, saves diffs as `.bad.html` or `.good.html` when mismatch occurs
* `:prompt_on_diff` - interactively choose which snapshot to keep (default: true)
* `:screenshot?` - generate screenshots of snapshot and baseline HTML (default: false)
"""
@spec html_snapshot(term(), Macro.Env.t(), module(), keyword()) :: term()
def html_snapshot(source, env, module, opts \\ []) do
opts = Keyword.put_new(opts, :prompt_on_diff, true)
if Keyword.get(opts, :cleanup?, false), do: cleanup_snapshots(module)
html = Excessibility.Source.to_html(source)
filename = get_filename(env, module, opts)
path = Path.join([File.cwd!(), @snapshots_path, filename])
File.mkdir_p!(@snapshots_path)
html
|> HTML.wrap()
|> maybe_diff_and_write(path, filename, opts)
|> maybe_open_browser(opts)
source
end
defp get_filename(env, module, opts) do
Keyword.get(opts, :name) ||
"#{module |> to_string() |> String.replace(".", "_")}_#{env.line}.html"
end
defp maybe_diff_and_write(new_html, path, filename, opts) do
system_mod = Application.get_env(:excessibility, :system_mod, Excessibility.System)
baseline_path = Path.join([@baseline_path, filename])
File.mkdir_p!(@baseline_path)
if File.exists?(baseline_path) do
old_html = File.read!(baseline_path)
if old_html != new_html do
Logger.warning("Snapshot differs from baseline: #{filename}")
if Keyword.get(opts, :tag_on_diff, true) do
bad_path = String.replace(path, ".html", ".bad.html")
good_path = String.replace(path, ".html", ".good.html")
File.write!(bad_path, new_html)
File.write!(good_path, old_html)
if Keyword.get(opts, :screenshot?, false) do
ensure_chromic_pdf_started()
bad_path |> screenshot_path() |> screenshot(new_html)
good_path |> screenshot_path() |> screenshot(old_html)
end
end
if Keyword.get(opts, :prompt_on_diff, true) do
Logger.warning("Prompting user to resolve diff")
bad_path = String.replace(path, ".html", ".bad.html")
good_path = String.replace(path, ".html", ".good.html")
File.write!(bad_path, new_html)
File.write!(good_path, old_html)
system_mod.open_with_system_cmd(good_path)
system_mod.open_with_system_cmd(bad_path)
IO.puts("\n[Excessibility] Snapshot differs from baseline: #{filename}")
IO.puts("Choose which version to keep:")
IO.puts("(g)ood (baseline) or (b)ad (new)?")
user_choice = ">> " |> IO.gets() |> String.trim()
selected =
case user_choice do
"g" -> old_html
"b" -> new_html
_ -> new_html
end
File.write!(baseline_path, selected)
Logger.info("Updated baseline with #{if selected == old_html, do: "good", else: "bad"} version")
else
Logger.info("Skipping diff prompt; baseline unchanged")
end
end
else
Logger.info("No baseline found for #{filename}, skipping diff")
end
File.write!(path, new_html)
Logger.info("Snapshot written to #{path}")
if Keyword.get(opts, :screenshot?, false) do
ensure_chromic_pdf_started()
path |> screenshot_path() |> screenshot(new_html)
end
path
end
defp screenshot_path(path), do: String.replace(path, ".html", ".png")
defp screenshot(output_path, html) do
ChromicPDF.capture_screenshot({:html, html}, output: output_path)
Logger.info("Wrote screenshot: #{output_path}")
rescue
e -> Logger.error("Screenshot failed: #{inspect(e)}")
end
defp ensure_chromic_pdf_started do
case Process.whereis(ChromicPDF) do
nil ->
case ChromicPDF.start_link(name: ChromicPDF) do
{:ok, _pid} ->
Logger.info("ChromicPDF process started for Excessibility screenshots")
{:error, {:already_started, _pid}} ->
:ok
{:error, reason} ->
Logger.error("Could not start ChromicPDF: #{inspect(reason)}")
end
_pid ->
:ok
end
rescue
exception -> Logger.error("Could not ensure ChromicPDF is running: #{Exception.message(exception)}")
end
defp maybe_open_browser(path, opts) do
if Keyword.get(opts, :open_browser?, false) do
mod = Application.get_env(:excessibility, :system_mod, Excessibility.System)
mod.open_with_system_cmd(path)
end
end
defp cleanup_snapshots(module) do
prefix = module |> to_string() |> String.replace(".", "_")
@snapshots_path
|> Path.join("#{prefix}_*.html")
|> Path.wildcard()
|> Enum.each(&File.rm/1)
Logger.info("Old snapshots for #{module} cleaned up")
end
end