Packages
Library for accessibility snapshot testing in Phoenix applications using axe-core and Playwright.
Current section
Files
Jump to
Current section
Files
lib/telemetry_capture/diff.ex
defmodule Excessibility.TelemetryCapture.Diff do
@moduledoc """
Computes differences between sequential telemetry snapshots.
Identifies added, changed, and removed assigns to highlight
what actually changed between LiveView events.
"""
@doc """
Computes the diff between current and previous assigns.
Returns nil if previous is nil (first snapshot).
Returns a map with :added, :changed, :removed keys.
"""
def compute_diff(_current, nil), do: nil
def compute_diff(current, previous) when is_map(current) and is_map(previous) do
current_keys = MapSet.new(Map.keys(current))
previous_keys = MapSet.new(Map.keys(previous))
added_keys = MapSet.difference(current_keys, previous_keys)
removed_keys = MapSet.difference(previous_keys, current_keys)
common_keys = MapSet.intersection(current_keys, previous_keys)
added =
Map.new(added_keys, &{&1, Map.get(current, &1)})
changed =
Enum.reduce(common_keys, %{}, fn key, acc ->
current_val = Map.get(current, key)
previous_val = Map.get(previous, key)
cond do
current_val == previous_val ->
acc
is_map(current_val) and is_map(previous_val) and not is_struct(current_val) and not is_struct(previous_val) ->
detect_nested_changes(to_string(key), current_val, previous_val, acc)
true ->
Map.put(acc, to_string(key), {previous_val, current_val})
end
end)
%{
added: added,
changed: changed,
removed: Enum.to_list(removed_keys)
}
end
@doc """
Extracts changes from a diff into a simple map format.
Converts added/changed/removed into a flat map of field => {old, new} tuples.
"""
def extract_changes(nil), do: nil
def extract_changes(%{added: added, changed: changed, removed: _removed}) do
added_changes =
Map.new(added, fn {key, val} -> {to_string(key), {nil, val}} end)
Map.merge(added_changes, changed)
end
# Private helpers
defp detect_nested_changes(path, current, previous, acc)
when is_map(current) and is_map(previous) and not is_struct(current) and not is_struct(previous) do
Enum.reduce(current, acc, fn {key, val}, nested_acc ->
prev_val = Map.get(previous, key)
nested_path = "#{path}.#{key}"
cond do
prev_val == nil -> Map.put(nested_acc, nested_path, {nil, val})
val != prev_val -> detect_nested_changes(nested_path, val, prev_val, nested_acc)
true -> nested_acc
end
end)
end
defp detect_nested_changes(path, current, previous, acc) do
Map.put(acc, path, {previous, current})
end
end