defmodule Mix.Tasks.LiveInteractionContracts.Compare do @shortdoc "Diff two harness result sets (e.g. before/after a LiveView upgrade)" @moduledoc """ The version-upgrade fuse. Runs the harness against two LiveView versions and reports which browser-native machines changed patch-safety behavior — so an interaction-heavy app can see, before merging a LiveView bump, exactly which delegated machines drifted. # produce two result sets: mix live_interaction_contracts.test --suite harness --lv 1.2.7 # writes priv/harness/results.json cp priv/harness/results.json /tmp/a.json mix live_interaction_contracts.test --suite harness --lv "github:phoenixframework/phoenix_live_view#main" cp priv/harness/results.json /tmp/b.json # diff them: mix live_interaction_contracts.compare --a /tmp/a.json --b /tmp/b.json Exits non-zero if any machine's survival changed (a drift is a merge-blocking signal). "No drift" = the observed behavior the contracts rely on is unchanged. """ use Mix.Task @nondestructive ~w(siblingPatch contentPatch attributePatch reorderNearby streamNearby pushPatch reconnect) @impl true def run(argv) do {opts, _, _} = OptionParser.parse(argv, strict: [a: :string, b: :string]) a = load(opts[:a] || Mix.raise("--a required")) b = load(opts[:b] || Mix.raise("--b required")) va = get_in(a, ["versions", "live_view"]) || "?" vb = get_in(b, ["versions", "live_view"]) || "?" engines = Map.keys(a["engines"]) |> Enum.filter(&Map.has_key?(b["engines"], &1)) diffs = for e <- engines, machine <- machine_keys(a, e), patch <- @nondestructive, sa = survived(machine, cell(a, e, machine, patch)), sb = survived(machine, cell(b, e, machine, patch)), sa != sb do " #{e} · #{machine} · #{patch}: #{va}=#{surv(sa)} → #{vb}=#{surv(sb)}" end Mix.shell().info("== compare LiveView #{va} → #{vb} (engines: #{Enum.join(engines, ", ")})") if diffs == [] do Mix.shell().info("✓ no drift — every tested machine has identical patch-safety on both versions") else Mix.shell().error("DRIFT DETECTED (#{length(diffs)}):\n" <> Enum.join(diffs, "\n")) Mix.raise("interaction behavior drifted between #{va} and #{vb} — review before upgrading") end end defp load(path), do: File.read!(path) |> JSON.decode!() defp machine_keys(data, e), do: (data["engines"][e]["runs"] |> hd() |> Map.keys()) -- ["ime"] defp cell(data, e, machine, patch), do: get_in(data, ["engines", e, "runs", Access.at(0), machine, "patches", patch, "after"]) defp survived(_m, nil), do: nil defp survived("pop", a), do: a["open"] == true defp survived("dlg", a), do: a["open"] == true defp survived("det", a), do: a["open"] == true defp survived("sel", a), do: a["value"] == "b" defp survived("inp", a), do: a["value"] == "typed-value" and a["focused"] == true defp survived("scr", a), do: a["scrollTop"] == 500 defp survived(_, _), do: nil defp surv(true), do: "green" defp surv(false), do: "red" defp surv(nil), do: "n/a" end