defmodule DiogramosWeb.CanvasLiveTest do
use DiogramosWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Diogramos.AccountsFixtures
import Diogramos.DiagramsFixtures
alias Diogramos.Accounts.Scope
alias Diogramos.Diagrams
describe "CanvasLive.Index" do
setup %{conn: conn} do
user = user_fixture()
conn = log_in_user(conn, user)
%{conn: conn, scope: Scope.for_user(user)}
end
test "renders empty state then a created canvas", %{conn: conn, scope: scope} do
{:ok, _lv, html} = live(conn, ~p"/canvases")
assert html =~ "No canvases yet"
_canvas = canvas_fixture(scope, %{slug: "first-canvas", title: "First"})
{:ok, _lv, html} = live(conn, ~p"/canvases")
assert html =~ "First"
assert html =~ "first-canvas"
end
test "the new-canvas modal opens and validates", %{conn: conn} do
{:ok, lv, _} = live(conn, ~p"/canvases")
lv |> element("#btn-new-canvas") |> render_click()
assert has_element?(lv, "#new-canvas-modal")
end
test "card carries the canvas's theme as data-theme", %{conn: conn, scope: scope} do
_ = canvas_fixture(scope, %{slug: "themed-card", title: "Themed", theme: "forest"})
{:ok, _lv, html} = live(conn, ~p"/canvases")
assert html =~ ~s(data-theme="forest")
end
test "manage modal opens for an owned canvas and lists current grants", %{
conn: conn,
scope: scope
} do
canvas = canvas_fixture(scope, %{slug: "managed-canvas", title: "Managed"})
collaborator = user_fixture()
{:ok, _} =
Diagrams.grant_permission(
"canvas",
canvas.id,
"user",
collaborator.id,
"editor",
granted_by_id: scope.user.id
)
{:ok, lv, _} = live(conn, ~p"/canvases")
lv
|> element("button[phx-click=\"open_manage_modal\"][phx-value-id=\"#{canvas.id}\"]")
|> render_click()
html = render(lv)
assert html =~ "Manage collaborators"
assert html =~ collaborator.email
assert html =~ "editor"
end
test "grant_user adds a collaborator and revoke_user removes them", %{
conn: conn,
scope: scope
} do
canvas = canvas_fixture(scope, %{slug: "share-flow", title: "Share"})
collaborator = user_fixture()
{:ok, lv, _} = live(conn, ~p"/canvases")
lv
|> element("button[phx-click=\"open_manage_modal\"][phx-value-id=\"#{canvas.id}\"]")
|> render_click()
lv
|> element("#manage-grant-form")
|> render_submit(%{"email" => collaborator.email, "role" => "viewer"})
assert Diagrams.effective_role(Scope.for_user(collaborator), canvas) == "viewer"
lv
|> element("button[phx-click=\"revoke_user\"][phx-value-user_id=\"#{collaborator.id}\"]")
|> render_click()
assert Diagrams.effective_role(Scope.for_user(collaborator), canvas) == nil
end
test "manage modal can switch the canvas theme", %{conn: conn, scope: scope} do
canvas =
canvas_fixture(scope, %{slug: "theme-switch", title: "Theme switch", theme: "afterdark"})
{:ok, lv, _} = live(conn, ~p"/canvases")
lv
|> element("button[phx-click=\"open_manage_modal\"][phx-value-id=\"#{canvas.id}\"]")
|> render_click()
render_hook(lv, "update_canvas_theme", %{"theme" => "forest"})
reloaded = Diagrams.get_canvas!(scope, canvas.id)
assert reloaded.theme == "forest"
assert render(lv) =~ "Theme updated."
end
test "grant_user surfaces an error when the email is unknown", %{conn: conn, scope: scope} do
canvas = canvas_fixture(scope, %{slug: "share-bad-email", title: "Bad email"})
{:ok, lv, _} = live(conn, ~p"/canvases")
lv
|> element("button[phx-click=\"open_manage_modal\"][phx-value-id=\"#{canvas.id}\"]")
|> render_click()
lv
|> element("#manage-grant-form")
|> render_submit(%{"email" => "ghost@example.com", "role" => "editor"})
assert render(lv) =~ "No registered user with that email."
end
test "filtering by folder narrows the canvas list", %{conn: conn, scope: scope} do
_outside = canvas_fixture(scope, %{slug: "outside-folder", title: "Outside"})
folder = folder_fixture(scope)
inside =
canvas_fixture(scope, %{slug: "inside-folder", title: "Inside", folder_id: folder.id})
{:ok, lv, _} = live(conn, ~p"/canvases")
assert render(lv) =~ "Outside"
lv
|> element("button", folder.name)
|> render_click()
html = render(lv)
assert html =~ inside.title
refute html =~ "Outside"
end
end
describe "CanvasLive.Edit" do
setup %{conn: conn} do
user = user_fixture()
scope = Scope.for_user(user)
canvas = canvas_fixture(scope, %{slug: "edit-canvas", title: "Edit me"})
Diogramos.DataCase.allow_authority(canvas.id)
%{conn: log_in_user(conn, user), scope: scope, canvas: canvas}
end
test "renders the editor shell", %{conn: conn, canvas: canvas} do
{:ok, _lv, html} = live(conn, ~p"/c/#{canvas.slug}")
assert html =~ canvas.title
assert html =~ ~s(id="canvas-svg")
assert html =~ ~s(id="tool-rect")
assert html =~ ~s(id="tool-fit-camera")
end
test "viewer-only access marks the editor read-only", %{
conn: conn,
canvas: canvas,
scope: owner_scope
} do
visitor = user_fixture()
{:ok, _} =
Diagrams.grant_permission("canvas", canvas.id, "user", visitor.id, "viewer",
granted_by_id: owner_scope.user.id
)
conn = conn |> Plug.Conn.delete_session(:user_token) |> log_in_user(visitor)
{:ok, lv, html} = live(conn, ~p"/c/#{canvas.slug}")
assert html =~ "view only"
assert lv |> element("#tool-rect") |> render() =~ "disabled"
end
test "missing canvas redirects to index", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/canvases"}}} =
live(conn, ~p"/c/does-not-exist")
end
test "apply_op insert_element renders the new shape", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01TESTRECT0000000000000000",
"type" => "rect",
"x" => 10,
"y" => 20,
"w" => 100,
"h" => 60,
"label" => "hello"
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => element}})
html = render(lv)
assert html =~ ~s(id="el-01TESTRECT0000000000000000")
assert html =~ "hello"
end
test "selecting an element renders the property panel form", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01ELEM00000000000000000000",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40,
"label" => "hi"
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => element}})
render_hook(lv, "select_element", %{"id" => element["id"]})
html = render(lv)
assert html =~ ~s(id="el-form-01ELEM00000000000000000000")
assert html =~ ~s(name="props[label]")
end
test "update_selected applies a patch and is held by the authority", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01ELEM11111111111111111111",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40,
"label" => "before"
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => element}})
render_hook(lv, "select_element", %{"id" => element["id"]})
render_hook(lv, "update_selected", %{
"props" => %{"label" => "after", "x" => "10", "y" => "20", "w" => "80", "h" => "40"}
})
{:ok, %{document: doc, version: version}} =
Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert doc["elements"][element["id"]]["label"] == "after"
assert doc["elements"][element["id"]]["x"] == 10
assert version > canvas.version
end
test "delete_selected removes the element from the document", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01ELEM22222222222222222222",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => element}})
render_hook(lv, "select_element", %{"id" => element["id"]})
render_hook(lv, "delete_selected", %{})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
refute Map.has_key?(doc["elements"], element["id"])
end
test "inserting a connector renders the connector path", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01EA000000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40
}
b = %{
"id" => "01EB000000000000000000000B",
"type" => "rect",
"x" => 200,
"y" => 100,
"w" => 80,
"h" => 40
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
connector = %{
"id" => "01CN0000000000000000000001",
"from" => %{"element" => a["id"], "anchor" => "auto", "_other" => b["id"]},
"to" => %{"element" => b["id"], "anchor" => "auto", "_other" => a["id"]}
}
render_hook(lv, "apply_op", %{
"op" => %{"type" => "insert_connector", "connector" => connector}
})
html = render(lv)
assert html =~ ~s(id="con-01CN0000000000000000000001")
assert html =~ ~s(marker-end=)
end
test "mermaid export modal renders the source", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01MMD0000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30,
"label" => "Hello"
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
lv |> element("#btn-export-mermaid") |> render_click()
html = render(lv)
assert html =~ "flowchart LR"
assert html =~ "Hello"
end
test "embed toggle generates and clears the token", %{
conn: conn,
canvas: canvas,
scope: scope
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
lv |> element("#btn-toggle-embed") |> render_click()
reloaded = Diagrams.get_canvas!(scope, canvas.id)
assert reloaded.embed_token
lv |> element("#btn-toggle-embed") |> render_click()
assert is_nil(Diagrams.get_canvas!(scope, canvas.id).embed_token)
end
test "clipboard preview surfaces in the property panel", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01CLP00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30,
"style" => %{"fill" => "primary", "stroke" => "accent"}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
assert render(lv) =~ "Clipboard empty"
render_hook(lv, "select_element", %{"id" => a["id"]})
render_hook(lv, "copy_style", %{})
html = render(lv)
refute html =~ "Clipboard empty"
assert html =~ "var(--color-primary)"
assert html =~ "var(--color-accent)"
assert html =~ ~s(<span class="ml-auto opacity-50 font-mono">element</span>)
end
test "new shape inherits the clipboard style", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
template = %{
"id" => "01CLPTPL00000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30,
"style" => %{"fill" => "warning", "stroke" => "primary", "shadow" => true}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => template}})
render_hook(lv, "select_element", %{"id" => template["id"]})
render_hook(lv, "copy_style", %{})
fresh = %{
"id" => "01CLPNEW00000000000000000B",
"type" => "rect",
"x" => 200,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => fresh}})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert doc["elements"][fresh["id"]]["style"]["fill"] == "warning"
assert doc["elements"][fresh["id"]]["style"]["stroke"] == "primary"
assert doc["elements"][fresh["id"]]["style"]["shadow"] == true
end
test "text element honors font/size/style controls", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
t = %{
"id" => "01TXT00000000000000000000A",
"type" => "text",
"x" => 50,
"y" => 60,
"w" => 100,
"h" => 30,
"label" => "B612 sample",
"style" => %{
"font_family" => "b612-mono",
"font_size" => 28,
"font_bold" => true,
"font_italic" => true,
"fill" => "primary"
}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => t}})
html = render(lv)
assert html =~ "B612 Mono"
assert html =~ "font-size: 28px"
assert html =~ "font-weight: 700"
assert html =~ "font-style: italic"
assert html =~ "fill: var(--color-primary)"
end
test "z-order controls reorder the document", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01ZA00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30
}
b = %{
"id" => "01ZB00000000000000000000B",
"type" => "rect",
"x" => 50,
"y" => 0,
"w" => 60,
"h" => 30
}
c = %{
"id" => "01ZC00000000000000000000C",
"type" => "rect",
"x" => 100,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => c}})
render_hook(lv, "select_element", %{"id" => a["id"]})
render_hook(lv, "z_order", %{"action" => "to_front"})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert List.last(doc["order"]) == a["id"]
render_hook(lv, "select_element", %{"id" => c["id"]})
render_hook(lv, "z_order", %{"action" => "to_back"})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert hd(doc["order"]) == c["id"]
end
test "transparent fill renders without a color var", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01TRP00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30,
"style" => %{"fill" => "transparent", "stroke" => "primary"}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
html = render(lv)
assert html =~ "fill: transparent"
assert html =~ "stroke: var(--color-primary)"
end
test "shadow toggle renders a translucent backing shape", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01SHDW0000000000000000000A",
"type" => "rect",
"x" => 50,
"y" => 50,
"w" => 80,
"h" => 40,
"style" => %{"fill" => "primary", "shadow" => true}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
html = render(lv)
assert html =~ "opacity: 0.55"
# 50 + shadow_offset (12) = 62
assert html =~ ~s(x="62")
end
test "snap-to-grid toggle exposes the grid pattern", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
refute render(lv) =~ "url(#canvas-grid-pattern)"
lv |> element("#btn-toggle-grid") |> render_click()
html = render(lv)
assert html =~ ~s(data-snap-grid="true")
assert html =~ "url(#canvas-grid-pattern)"
end
test "set_grid event applies a stored preference on mount", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
render_hook(lv, "set_grid", %{"on" => true})
html = render(lv)
assert html =~ ~s(data-snap-grid="true")
end
test "select_set replaces selection with multiple items", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01MS00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30
}
b = %{
"id" => "01MS00000000000000000000B",
"type" => "rect",
"x" => 80,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "select_set", %{"elements" => [a["id"], b["id"]], "connectors" => []})
html = render(lv)
assert html =~ "multi-selection-panel"
assert html =~ ~s(<span class="font-mono">2</span>)
assert html =~ "elements"
assert html =~ "connectors"
end
test "copy + paste selection clones elements with new ids and an offset", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01CP00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30
}
b = %{
"id" => "01CP00000000000000000000B",
"type" => "rect",
"x" => 80,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "select_set", %{"elements" => [a["id"], b["id"]], "connectors" => []})
render_hook(lv, "copy_selection", %{})
render_hook(lv, "paste_selection", %{})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
ids = Map.keys(doc["elements"])
assert length(ids) == 4
pasted = ids -- [a["id"], b["id"]]
assert length(pasted) == 2
# Pasted shapes are offset from their originals.
pasted_xs = pasted |> Enum.map(&doc["elements"][&1]["x"]) |> Enum.sort()
assert pasted_xs == [24, 104]
end
test "copy + paste selection remaps connector endpoints to new element ids", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01CC00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30
}
b = %{
"id" => "01CC00000000000000000000B",
"type" => "rect",
"x" => 100,
"y" => 0,
"w" => 60,
"h" => 30
}
k = %{
"id" => "01CK00000000000000000000K",
"from" => %{"element" => a["id"], "anchor" => "auto"},
"to" => %{"element" => b["id"], "anchor" => "auto"}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_connector", "connector" => k}})
render_hook(lv, "select_set", %{
"elements" => [a["id"], b["id"]],
"connectors" => [k["id"]]
})
render_hook(lv, "copy_selection", %{})
render_hook(lv, "paste_selection", %{})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert map_size(doc["elements"]) == 4
assert map_size(doc["connectors"]) == 2
[orig, copy] =
doc["connectors"]
|> Map.values()
|> Enum.sort_by(& &1["id"])
# Original and copy point to different element pairs.
refute orig["from"]["element"] == copy["from"]["element"]
refute orig["to"]["element"] == copy["to"]["element"]
end
test "delete_selected removes every item in a multi-selection", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01MD00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30
}
b = %{
"id" => "01MD00000000000000000000B",
"type" => "rect",
"x" => 80,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "select_set", %{"elements" => [a["id"], b["id"]], "connectors" => []})
render_hook(lv, "delete_selected", %{})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert doc["elements"] == %{}
end
test "label position + size apply to shape labels", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01LBL00000000000000000000A",
"type" => "rect",
"x" => 100,
"y" => 50,
"w" => 200,
"h" => 100,
"label" => "centered",
"style" => %{"label_position" => "center", "label_size" => "lg"}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
html = render(lv)
assert html =~ ~s(text-anchor="middle")
assert html =~ ~s(dominant-baseline="middle")
assert html =~ "font-size: 18px"
end
test "label font/color/weight apply to shape labels", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01LBL00000000000000000000B",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 100,
"h" => 60,
"label" => "Hello",
"style" => %{
"label_color" => "primary",
"label_font_family" => "b612-mono",
"label_bold" => true,
"label_italic" => true
}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
html = render(lv)
assert html =~ "fill: var(--color-primary)"
assert html =~ "B612 Mono"
assert html =~ "font-weight: 700"
assert html =~ "font-style: italic"
end
test "multiple metadata links render side-by-side icons", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01LNK00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 200,
"h" => 100,
"links" => [
%{
"enabled" => true,
"kind" => "external",
"target" => "https://elixir-lang.org",
"icon" => "globe-alt"
},
%{
"enabled" => true,
"kind" => "canvas",
"target" => "another-canvas",
"icon" => "link"
}
]
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
html = render(lv)
assert html =~ ~s(href="https://elixir-lang.org")
# The editor renders canvas links with the :editor context (link_href/2),
# which points at the canvas editor (/c/<slug>); /c-embed/<slug> is only
# used in the read-only embed view.
assert html =~ ~s(href="/c/another-canvas")
assert html =~ "dx-link-icon-globe-alt"
assert html =~ "dx-link-icon-link"
end
test "add_link / remove_link append + drop links", %{
conn: conn,
canvas: canvas
} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01LNK00000000000000000000B",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 100,
"h" => 60
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
render_hook(lv, "select_element", %{"id" => el["id"]})
render_hook(lv, "add_link", %{"element_id" => el["id"]})
render_hook(lv, "add_link", %{"element_id" => el["id"]})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert length(doc["elements"][el["id"]]["links"]) == 2
render_hook(lv, "remove_link", %{"element_id" => el["id"], "index" => "0"})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert length(doc["elements"][el["id"]]["links"]) == 1
end
test "border dash + width style applies to shapes", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01BRD0000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40,
"style" => %{"dash" => "dashed", "stroke_width" => 4}
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
html = render(lv)
assert html =~ "stroke-width: 4"
assert html =~ "stroke-dasharray: 8 6"
end
test "selected element shows a resize handle in select mode", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
el = %{
"id" => "01RSZ00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => el}})
render_hook(lv, "select_element", %{"id" => el["id"]})
assert render(lv) =~ ~s(data-resize-handle="01RSZ00000000000000000000A")
end
test "copy/paste style clones colors between elements", %{conn: conn, canvas: canvas} do
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
a = %{
"id" => "01STY00000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 60,
"h" => 30,
"style" => %{"fill" => "primary", "stroke" => "accent"}
}
b = %{
"id" => "01STY00000000000000000000B",
"type" => "rect",
"x" => 100,
"y" => 0,
"w" => 60,
"h" => 30
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => a}})
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => b}})
render_hook(lv, "select_element", %{"id" => a["id"]})
render_hook(lv, "copy_style", %{})
render_hook(lv, "select_element", %{"id" => b["id"]})
render_hook(lv, "paste_style", %{})
{:ok, %{document: doc}} = Diogramos.Diagrams.Authority.snapshot(canvas.id)
assert doc["elements"][b["id"]]["style"]["fill"] == "primary"
assert doc["elements"][b["id"]]["style"]["stroke"] == "accent"
end
test "cursor presence broadcasts to peers", %{
conn: conn,
canvas: canvas,
scope: owner_scope
} do
collaborator = user_fixture()
{:ok, _} =
Diogramos.Diagrams.grant_permission(
"canvas",
canvas.id,
"user",
collaborator.id,
"editor",
granted_by_id: owner_scope.user.id
)
{:ok, lv_a, _} = live(conn, ~p"/c/#{canvas.slug}")
conn_b =
Phoenix.ConnTest.build_conn()
|> Phoenix.ConnTest.init_test_session(%{})
|> log_in_user(collaborator)
{:ok, lv_b, _} = live(conn_b, ~p"/c/#{canvas.slug}")
render_hook(lv_a, "cursor", %{"x" => 123, "y" => 45})
# Let the presence diff propagate.
_ = :sys.get_state(lv_b.pid)
html = render(lv_b)
assert html =~ ~s(id="cur-user-)
assert html =~ "translate(123 45)"
end
test "drag locks ghost the element for peers", %{
conn: conn,
canvas: canvas,
scope: owner_scope
} do
collaborator = user_fixture()
{:ok, _} =
Diogramos.Diagrams.grant_permission(
"canvas",
canvas.id,
"user",
collaborator.id,
"editor",
granted_by_id: owner_scope.user.id
)
{:ok, lv_a, _} = live(conn, ~p"/c/#{canvas.slug}")
conn_b =
Phoenix.ConnTest.build_conn()
|> Phoenix.ConnTest.init_test_session(%{})
|> log_in_user(collaborator)
{:ok, lv_b, _} = live(conn_b, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01LOCK0000000000000000000A",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40
}
render_hook(lv_a, "apply_op", %{
"op" => %{"type" => "insert_element", "element" => element}
})
render_hook(lv_a, "set_lock", %{"id" => element["id"]})
_ = :sys.get_state(lv_b.pid)
html = render(lv_b)
assert html =~ ~s(id="el-01LOCK0000000000000000000A")
assert html =~ "opacity-40"
end
test "two LV processes converge via the authority broadcast", %{
conn: conn,
canvas: canvas,
scope: owner_scope
} do
collaborator = user_fixture()
{:ok, _} =
Diogramos.Diagrams.grant_permission(
"canvas",
canvas.id,
"user",
collaborator.id,
"editor",
granted_by_id: owner_scope.user.id
)
{:ok, lv_a, _} = live(conn, ~p"/c/#{canvas.slug}")
conn_b =
Phoenix.ConnTest.build_conn()
|> Phoenix.ConnTest.init_test_session(%{})
|> log_in_user(collaborator)
{:ok, lv_b, _} = live(conn_b, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01CONV0000000000000000000F",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 80,
"h" => 40,
"label" => "from-A"
}
render_hook(lv_a, "apply_op", %{
"op" => %{"type" => "insert_element", "element" => element}
})
# The authority broadcasts to lv_b; render after a brief sync.
_ = :sys.get_state(lv_b.pid)
assert render(lv_b) =~ "from-A"
end
test "apply_op rejected when role is viewer", %{
conn: conn,
canvas: canvas,
scope: owner_scope
} do
visitor = user_fixture()
{:ok, _} =
Diagrams.grant_permission("canvas", canvas.id, "user", visitor.id, "viewer",
granted_by_id: owner_scope.user.id
)
conn = conn |> Plug.Conn.delete_session(:user_token) |> log_in_user(visitor)
{:ok, lv, _} = live(conn, ~p"/c/#{canvas.slug}")
element = %{
"id" => "01TESTRECT0000000000000000",
"type" => "rect",
"x" => 0,
"y" => 0,
"w" => 50,
"h" => 50
}
render_hook(lv, "apply_op", %{"op" => %{"type" => "insert_element", "element" => element}})
refute render(lv) =~ "el-01TESTRECT0000000000000000"
end
end
end
neiam
/diogramos
diogramos
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
31.6 KiB · text
History
dc2edab