#!/usr/bin/env elixir
# Menu reference conformance fixture.
# Dogfoods the SHIPPED component (lib/.../components/menu.ex) and the SHIPPED hook
# (priv/static/lic_hooks.js) — the conformance suite tests the real artifacts, not a
# re-implementation.
# Run: elixir app.exs (http://127.0.0.1:4130)
Mix.install([
{:phoenix, "~> 1.8"},
{:phoenix_live_view, "~> 1.1"},
{:bandit, "~> 1.7"},
{:jason, "~> 1.4"}
])
Application.put_env(:phoenix, :json_library, Jason)
# Load the real shipped component.
# Compiling the shipped component extracts its colocated hook into this fixture's own
# build path; ColocatedAssets.compile() writes the index.js manifest — the same
# pipeline a consumer's `mix compile` runs. Served at /colocated, imported as a real
# ES module, so the suite tests the true shipped artifact under the swept LV version.
Code.require_file(Path.join(__DIR__, "../../lib/live_interaction_contracts/components/menu.ex"))
Phoenix.LiveView.ColocatedAssets.compile()
colocated_dir =
Path.join([Mix.Project.build_path(), "phoenix-colocated", to_string(Mix.Project.config()[:app] || "")])
Application.put_env(:pr, :colocated_dir, colocated_dir)
Application.put_env(:pr, PRWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4130],
server: true,
adapter: Bandit.PhoenixAdapter,
live_view: [signing_salt: "menu-ref-salt"],
secret_key_base: String.duplicate("menu-reference-secret!!", 4),
pubsub_server: PR.PubSub,
check_origin: false,
debug_errors: true
)
defmodule PRWeb.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"""
Menu reference
<%= @inner_content %>
"""
end
end
defmodule PRWeb.MainLive do
use Phoenix.LiveView
import LiveInteractionContracts.Components.Menu
@item_ids ~w(alpha beta gamma)
def mount(_p, _s, socket) do
{:ok,
assign(socket,
gamma_rev: 0,
alpha_rev: 0,
attr_rev: 0,
sib: 0,
removed: MapSet.new(),
select_count: 0,
last_selected: "none",
spacer: 40
)}
end
def render(assigns) do
assigns = assign(assigns, items: build_items(assigns))
~H"""
<.menu id="menu" mode="auto" on_action="select" placement="bottom" data-rev={@attr_rev}>
<:trigger>Open menu
<:item :for={i <- @items} id={i.id}>{i.label}
sib {@sib}
{@select_count}:{@last_selected}
"""
end
defp build_items(assigns) do
for id <- @item_ids, id not in assigns.removed do
label =
case id do
"alpha" -> "Alpha #{assigns.alpha_rev}"
"gamma" -> "Gamma #{assigns.gamma_rev}"
"beta" -> "Beta"
end
%{id: id, label: label}
end
end
# Unrelated-item content patch (touches gamma; used to prove an active
# cursor elsewhere in the list is untouched by an unrelated patch).
def handle_event("content", _, s), do: {:noreply, update(s, :gamma_rev, &(&1 + 1))}
# Active-item content patch (touches alpha; the ignore_attributes money test —
# aria-activedescendant / data-active must survive this re-render).
def handle_event("item", _, s), do: {:noreply, update(s, :alpha_rev, &(&1 + 1))}
def handle_event("attr", _, s), do: {:noreply, update(s, :attr_rev, &(&1 + 1))}
def handle_event("sib", _, s), do: {:noreply, update(s, :sib, &(&1 + 1))}
def handle_event("move", _, s), do: {:noreply, assign(s, spacer: s.assigns.spacer + 150)}
# deep enough that the ~54px menu cannot fit below on any engine's font metrics
def handle_event("bottom", _, s), do: {:noreply, assign(s, spacer: 660)}
def handle_event("reset", _, s), do: {:noreply, assign(s, spacer: 40)}
def handle_event("remove_beta", _, s), do: {:noreply, update(s, :removed, &MapSet.put(&1, "beta"))}
# Selection is an event, never client-held state: the server owns the outcome.
def handle_event("select", %{"id" => id}, s) do
{:noreply, s |> update(:select_count, &(&1 + 1)) |> assign(:last_selected, id)}
end
end
defmodule PRWeb.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: {PRWeb.Layout, :root}
end
scope "/" do
pipe_through :browser
live "/", PRWeb.MainLive
end
end
defmodule PRWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :pr
@session_options [store: :cookie, key: "_pr_key", signing_salt: "menuref", same_site: "Lax"]
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
plug Plug.Static,
at: "/colocated",
from: Application.compile_env!(:pr, :colocated_dir),
gzip: false
plug Plug.Session, @session_options
plug PRWeb.Router
end
{:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: PR.PubSub}, PRWeb.Endpoint], strategy: :one_for_one)
IO.puts("VERSIONS phoenix=#{Application.spec(:phoenix, :vsn)} live_view=#{Application.spec(:phoenix_live_view, :vsn)} elixir=#{System.version()} otp=#{System.otp_release()}")
IO.puts("READY http://127.0.0.1:4130")
Process.sleep(:infinity)