Current section
Files
Jump to
Current section
Files
priv/harness/app.exs
#!/usr/bin/env elixir
# live_interaction_contracts — multi-version conformance harness (test target).
#
# One LiveView renders every browser-native machine under test (popover, dialog,
# details, native select, focused input, scroll) plus shared "nearby" patch
# sources (keyed list, stream), and exposes a single generic `bump` event so the
# driver can patch any region's content / attribute / sibling in isolation.
#
# The LiveView version is selected by the LV_SPEC env var so the SAME app can run
# against multiple versions:
# LV_SPEC unset / "stable" -> {:phoenix_live_view, "~> 1.1"}
# LV_SPEC "1.2.7" -> {:phoenix_live_view, "1.2.7"}
# LV_SPEC "github:owner/repo#ref" -> git dependency at ref
# LV_SPEC "path:/abs/path" -> local path dependency
#
# Run: LV_SPEC=1.2.7 elixir app.exs (http://127.0.0.1:4123)
lv_dep =
case System.get_env("LV_SPEC") do
nil -> {:phoenix_live_view, "~> 1.1"}
"" -> {:phoenix_live_view, "~> 1.1"}
"stable" -> {:phoenix_live_view, "~> 1.1"}
"path:" <> path -> {:phoenix_live_view, path: path}
"github:" <> rest ->
case String.split(rest, "#", parts: 2) do
[repo, ref] -> {:phoenix_live_view, github: repo, ref: ref}
[repo] -> {:phoenix_live_view, github: repo}
end
version -> {:phoenix_live_view, version}
end
Mix.install([
{:phoenix, "~> 1.8"},
lv_dep,
{:bandit, "~> 1.7"},
{:jason, "~> 1.4"}
])
Application.put_env(:phoenix, :json_library, Jason)
Application.put_env(:lic, LICWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4123")],
server: true,
adapter: Bandit.PhoenixAdapter,
live_view: [signing_salt: "lic-harness-salt"],
secret_key_base: String.duplicate("live-interaction-contracts!", 4),
pubsub_server: LIC.PubSub,
check_origin: false,
debug_errors: true
)
defmodule LICWeb.Layout do
use Phoenix.Component
@phoenix_js File.read!(Application.app_dir(:phoenix, "priv/static/phoenix.min.js"))
@lv_js File.read!(Application.app_dir(:phoenix_live_view, "priv/static/phoenix_live_view.min.js"))
def render("root.html", assigns) do
assigns = assign(assigns, phoenix_js: @phoenix_js, lv_js: @lv_js)
~H"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<title>live_interaction_contracts harness</title>
<script><%= Phoenix.HTML.raw(@phoenix_js) %></script>
<script><%= Phoenix.HTML.raw(@lv_js) %></script>
<script>
window.__hookLog = [];
const mk = () => ({
mounted() { window.__hookLog.push({id: this.el.id, ev: "mounted"}); },
updated() { window.__hookLog.push({id: this.el.id, ev: "updated"}); },
destroyed() { window.__hookLog.push({id: this.el.id, ev: "destroyed"}); },
disconnected() { window.__hookLog.push({id: this.el.id, ev: "disconnected"}); },
reconnected() { window.__hookLog.push({id: this.el.id, ev: "reconnected"}); }
});
window.addEventListener("DOMContentLoaded", () => {
const csrf = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const ls = new LiveView.LiveSocket("/live", Phoenix.Socket, { hooks: {Track: mk()}, params: {_csrf_token: csrf} });
ls.connect();
window.liveSocket = ls;
});
</script>
</head>
<body><%= @inner_content %></body>
</html>
"""
end
end
defmodule LICWeb.MainLive do
use Phoenix.LiveView
alias Phoenix.LiveView.JS
def mount(_p, _s, socket) do
{:ok,
socket
|> assign(bumps: %{}, order: ["i1", "i2", "i3", "i4"], p: "none", aha_rev: 0)
|> stream(:sitems, for(i <- 1..3, do: %{id: "s#{i}", label: "S#{i}-v0"}))}
end
def handle_params(params, _uri, socket), do: {:noreply, assign(socket, p: params["p"] || "none")}
defp b(assigns, k), do: Map.get(assigns.bumps, k, 0)
def render(assigns) do
~H"""
<main>
<div id="banner" phx-hook="Track">banner-{b(assigns, "banner")}</div>
<button id="bump-banner" phx-click="bump" phx-value-k="banner">banner</button>
<%!-- shared nearby patch sources --%>
<ul id="near-list"><li :for={id <- @order} id={"o-" <> id}>{id}</li></ul>
<ul id="near-stream" phx-update="stream"><li :for={{d, it} <- @streams.sitems} id={d}>{it.label}</li></ul>
<button id="do-reorder" phx-click="reorder">reorder</button>
<button id="do-sinsert" phx-click="s_insert">s insert</button>
<button id="do-sreset" phx-click="s_reset">s reset</button>
<%!-- ===== POPOVER (browser top-layer, no attr reflection) ===== --%>
<button id="pop-invoker" popovertarget="pop">open popover</button>
<div id="pop" popover="manual" phx-hook="Track" data-rev={b(assigns, "pop_attr")} data-aha-rev={@aha_rev}>
<span id="pop-text">pop-{b(assigns, "pop_content")}</span>
<span id="pop-aha-content">aha-pop-{@aha_rev}</span>
</div>
<div id="pop-sib">sib-{b(assigns, "pop_sib")}</div>
<button id="bump-pop_content" phx-click="bump" phx-value-k="pop_content">c</button>
<button id="bump-pop_attr" phx-click="bump" phx-value-k="pop_attr">a</button>
<button id="bump-pop_sib" phx-click="bump" phx-value-k="pop_sib">s</button>
<%!-- ===== DIALOG (open reflected into `open` attribute) ===== --%>
<button id="aha-open-raw" onclick="document.getElementById('dlg').showModal()">open raw dialog</button>
<dialog id="dlg" phx-hook="Track" data-rev={b(assigns, "dlg_attr")} data-aha-rev={@aha_rev}>
<input id="dlg-input" />
<button id="dlg-btn2">second</button>
<button id="aha-patch-raw" phx-click="aha_patch">patch from raw dialog</button>
<span id="dlg-text">dlg-{b(assigns, "dlg_content")}</span>
<span id="dlg-aha-content">aha-raw-{@aha_rev}</span>
</dialog>
<%!-- Same machine and patch as #dlg; only its browser-owned state surface is protected. --%>
<button id="aha-open-protected" onclick="document.getElementById('dlg-protected').showModal()">
open protected dialog
</button>
<dialog
id="dlg-protected"
phx-hook="Track"
phx-mounted={JS.ignore_attributes(["open"])}
data-aha-rev={@aha_rev}
>
<button id="aha-patch-protected" phx-click="aha_patch">patch from protected dialog</button>
<span id="dlg-protected-aha-content">aha-protected-{@aha_rev}</span>
</dialog>
<div id="dlg-sib">sib-{b(assigns, "dlg_sib")}</div>
<button id="bump-dlg_content" phx-click="bump" phx-value-k="dlg_content">c</button>
<button id="bump-dlg_attr" phx-click="bump" phx-value-k="dlg_attr">a</button>
<button id="bump-dlg_sib" phx-click="bump" phx-value-k="dlg_sib">s</button>
<%!-- Focused causal trial: one acknowledged diff touches all three state holders. --%>
<button id="aha-patch" phx-click="aha_patch">Aha: patch all machines</button>
<%!-- ===== DETAILS/SUMMARY (open reflected into `open` attribute) ===== --%>
<details id="det" phx-hook="Track" data-rev={b(assigns, "det_attr")}>
<summary id="det-sum">summary</summary>
<div id="det-body">det-{b(assigns, "det_content")}</div>
</details>
<div id="det-sib">sib-{b(assigns, "det_sib")}</div>
<button id="bump-det_content" phx-click="bump" phx-value-k="det_content">c</button>
<button id="bump-det_attr" phx-click="bump" phx-value-k="det_attr">a</button>
<button id="bump-det_sib" phx-click="bump" phx-value-k="det_sib">s</button>
<%!-- ===== NATIVE SELECT (editing/selection state; popup not automatable) ===== --%>
<select id="sel" phx-hook="Track" data-rev={b(assigns, "sel_attr")}>
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C-{b(assigns, "sel_content")}</option>
</select>
<div id="sel-sib">sib-{b(assigns, "sel_sib")}</div>
<button id="bump-sel_content" phx-click="bump" phx-value-k="sel_content">c</button>
<button id="bump-sel_attr" phx-click="bump" phx-value-k="sel_attr">a</button>
<button id="bump-sel_sib" phx-click="bump" phx-value-k="sel_sib">s</button>
<%!-- ===== FOCUS / SELECTION / INPUT EDITING ===== --%>
<input id="inp" phx-hook="Track" value={"server-" <> to_string(b(assigns, "inp_val"))} />
<textarea id="ta" phx-hook="Track">ta-{b(assigns, "ta_content")}</textarea>
<div id="inp-sib">sib-{b(assigns, "inp_sib")}</div>
<button id="bump-inp_sib" phx-click="bump" phx-value-k="inp_sib">s</button>
<button id="bump-inp_val" phx-click="bump" phx-value-k="inp_val">v</button>
<%!-- ===== SCROLL ===== --%>
<div id="scroller" phx-hook="Track" data-rev={b(assigns, "scr_attr")}
style="height:100px;overflow:auto;border:1px solid #999">
<div :for={i <- 1..60} id={"row-#{i}"} style="height:20px">row {i} c{b(assigns, "scr_content")}</div>
</div>
<div id="scr-sib">sib-{b(assigns, "scr_sib")}</div>
<button id="bump-scr_content" phx-click="bump" phx-value-k="scr_content">c</button>
<button id="bump-scr_attr" phx-click="bump" phx-value-k="scr_attr">a</button>
<button id="bump-scr_sib" phx-click="bump" phx-value-k="scr_sib">s</button>
<%!-- ===== NAVIGATION ===== --%>
<div id="nav-el" phx-hook="Track" data-p={@p}>p={@p}</div>
<button id="do-patch" phx-click="do_patch">push_patch</button>
<button id="do-nav" phx-click="do_nav">push_navigate</button>
</main>
"""
end
def handle_event("bump", %{"k" => k}, s),
do: {:noreply, update(s, :bumps, &Map.update(&1, k, 1, fn n -> n + 1 end))}
def handle_event("aha_patch", _, s), do: {:noreply, update(s, :aha_rev, &(&1 + 1))}
def handle_event("reorder", _, s), do: {:noreply, assign(s, order: Enum.reverse(s.assigns.order))}
def handle_event("s_insert", _, s), do: {:noreply, stream_insert(s, :sitems, %{id: "sx#{System.unique_integer([:positive])}", label: "SX"}, at: 0)}
def handle_event("s_reset", _, s), do: {:noreply, stream(s, :sitems, for(i <- 1..3, do: %{id: "s#{i}", label: "S#{i}-R"}), reset: true)}
def handle_event("do_patch", _, s), do: {:noreply, push_patch(s, to: "/?p=patched")}
def handle_event("do_nav", _, s), do: {:noreply, push_navigate(s, to: "/other")}
end
defmodule LICWeb.OtherLive do
use Phoenix.LiveView
def mount(_p, _s, socket), do: {:ok, socket}
def render(assigns), do: ~H"""
<div id="other-page">other</div><button id="go-back" phx-click="back">back</button>
"""
def handle_event("back", _, s), do: {:noreply, push_navigate(s, to: "/")}
end
defmodule LICWeb.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :protect_from_forgery
plug :put_root_layout, html: {LICWeb.Layout, :root}
end
scope "/" do
pipe_through :browser
live "/", LICWeb.MainLive
live "/other", LICWeb.OtherLive
end
end
defmodule LICWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :lic
@session_options [store: :cookie, key: "_lic_key", signing_salt: "lic", same_site: "Lax"]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
plug Plug.Session, @session_options
plug LICWeb.Router
end
{:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: LIC.PubSub}, LICWeb.Endpoint], strategy: :one_for_one)
versions = %{
phoenix: to_string(Application.spec(:phoenix, :vsn)),
live_view: to_string(Application.spec(:phoenix_live_view, :vsn)),
elixir: System.version(),
otp: System.otp_release(),
lv_spec: System.get_env("LV_SPEC") || "stable"
}
File.write!(System.get_env("VERSIONS_OUT") || "versions.json", Jason.encode!(versions))
IO.puts("VERSIONS " <> Jason.encode!(versions))
IO.puts("READY http://127.0.0.1:#{System.get_env("PORT") || "4123"}")
Process.sleep(:infinity)