Hakushi.Test reads and changes the canvases of a LiveView in Phoenix.LiveViewTest, with no browser.

import Hakushi.Test

test "a foreign key joins columns of one type", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/schema")

  assert :ok = connect(view, "schema", {"orders", "user_id"}, {"users", "id"})
  assert %{target: "users"} = Enum.find(edges(view, "schema"), &(&1.source == "orders"))

  assert {:error, "These columns are not of one type."} =
           connect(view, "schema", {"orders", "total"}, {"users", "id"})
end

The functions send the events the browser sends, so each change goes the way of a person's: the rules, on_change, the room of a shared canvas. A change the canvas refuses gives {:error, reason}, with the words the person sees.

nodes/2, edges/2what the canvas shows, as maps (a node's :parent is its group)
move/4move(view, id, "node", {x, y}), or several with sizes, or maps with a :parent for a drop into a group
connect/4ends as a node's id or {node, handle}
reconnect/4source: or target:, the new end of an edge
remove/3nodes: and edges: (not delete, which Phoenix.ConnTest has)
label/4an edge's label
copy_paste/4, duplicate/3{:ok, ids} of the copies
drop/4an item, through new_node
undo/2, redo/2the LiveView's person's own changes
notice/2the reason the canvas shows for its last refusal
people/2the others on a shared canvas

A shared canvas in a test is shared with the other LiveViews of its topic in the test: open two, as two people, to test what one sees of the other. Start an Issho instance for the tests (Issho.Test.start!/1 starts one per test), and give its rooms a short life with its :ttl (50 milliseconds, say).

A change one person makes reaches the other's LiveView as a message: before you look at the other's canvas, let its LiveView handle its mailbox (:sys.get_state(view.pid), twice: once for the change, once for the update of the canvas it sends itself).