#!/usr/bin/env elixir # Cursor replace-with-same-id spike (test target). # # Tests the UNPROVEN client-owned tier: can client-owned coordination state # (active item / highlight / roving tabindex / aria-activedescendant) survive when # the DOM NODE is replaced but the logical item id stays the same? # # Prior audits never exercised this: LiveView preserved the STRONGER condition # (same id AND same node). Here we deliberately force node replacement while # keeping logical ids, via techniques whose actual replacement behavior the driver # verifies with a JS expando. # # Forced-replacement techniques exposed: # mode=tag : render each item as
  • or """ end # The list container carries the Cursor hook. Items are identity-bearing children # with BOTH a DOM id and a stable logical data-item id. def list(assigns) do ~H""" <.dynamic_container id="clist" tag={@container_tag}> <%= for it <- @items, it != @hide do %> <.dynamic_item tag={@item_tag} logical={it} rev={@rev} /> <% end %> """ end def dynamic_container(%{tag: "ul"} = assigns) do ~H""" """ end def dynamic_container(%{tag: "ol"} = assigns) do ~H"""
      {render_slot(@inner_block)}
    """ end def dynamic_item(%{tag: "li"} = assigns) do ~H"""
  • @logical} data-item={@logical} role="option">{@logical} v{@rev}
  • """ end def dynamic_item(%{tag: "button"} = assigns) do ~H""" """ end def handle_event("content", _, s), do: {:noreply, assign(s, rev: s.assigns.rev + 1)} def handle_event("item_tag", _, s), do: {:noreply, assign(s, item_tag: swap(s.assigns.item_tag, "li", "button"))} def handle_event("container_tag", _, s), do: {:noreply, assign(s, container_tag: swap(s.assigns.container_tag, "ul", "ol"))} def handle_event("wrap", _, s), do: {:noreply, assign(s, wrap: not s.assigns.wrap)} def handle_event("hide", %{"id" => id}, s), do: {:noreply, assign(s, hide: id)} def handle_event("show_all", _, s), do: {:noreply, assign(s, hide: nil)} defp swap(cur, a, b), do: if(cur == a, do: b, else: a) end defmodule CSWeb.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: {CSWeb.Layout, :root} end scope "/" do pipe_through :browser live "/", CSWeb.MainLive end end defmodule CSWeb.Endpoint do use Phoenix.Endpoint, otp_app: :cs @session_options [store: :cookie, key: "_cs_key", signing_salt: "cursor", same_site: "Lax"] socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] plug Plug.Session, @session_options plug CSWeb.Router end {:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: CS.PubSub}, CSWeb.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:4126") Process.sleep(:infinity)