diff --git a/lib/live_debugger/structs/tree_node.ex b/lib/live_debugger/structs/tree_node.ex index e012d1d2a..52bce8c81 100644 --- a/lib/live_debugger/structs/tree_node.ex +++ b/lib/live_debugger/structs/tree_node.ex @@ -48,17 +48,6 @@ defmodule LiveDebugger.Structs.TreeNode do end end - @doc """ - Same as `id_from_string/1`, but raises an ArgumentError if the ID is invalid. - """ - @spec id_from_string!(id :: String.t()) :: id() - def id_from_string!(string) do - case id_from_string(string) do - {:ok, id} -> id - :error -> raise ArgumentError, "Invalid ID: #{inspect(string)}" - end - end - @doc """ Adds a child to the parent node. """ diff --git a/test/structs/lv_process_test.exs b/test/structs/lv_process_test.exs new file mode 100644 index 000000000..410f5b4e2 --- /dev/null +++ b/test/structs/lv_process_test.exs @@ -0,0 +1,235 @@ +defmodule LiveDebugger.Structs.LvProcessTest do + use ExUnit.Case, async: true + + import Mox + + alias LiveDebugger.Structs.LvProcess + + describe "new/2" do + test "creates a new LvProcess struct with the given pid and socket" do + pid = self() + + socket = %Phoenix.LiveView.Socket{ + id: "socket_id", + root_pid: pid, + parent_pid: nil, + transport_pid: nil, + view: LiveDebuggerTest.TestView, + host_uri: :not_mounted_at_router + } + + lv_process = LvProcess.new(pid, socket) + + assert %LvProcess{ + socket_id: "socket_id", + root_pid: ^pid, + parent_pid: nil, + pid: ^pid, + transport_pid: nil, + module: LiveDebuggerTest.TestView, + nested?: false, + debugger?: false, + embedded?: true + } = lv_process + end + end + + describe "new/1" do + test "returns nil if the process is not found" do + LiveDebugger.MockProcessService + |> expect(:state, fn _pid -> + {:error, :not_alive} + end) + + assert LvProcess.new(self()) == nil + end + + test "returns a new LvProcess struct if the process is found" do + socket_id = "socket_id" + pid = self() + root_pid = pid + parent_pid = nil + transport_pid = nil + module = LiveDebuggerTest.TestView + + LiveDebugger.MockProcessService + |> expect(:state, fn _pid -> + {:ok, + LiveDebugger.Fakes.state( + socket_id: socket_id, + root_pid: root_pid, + parent_pid: parent_pid, + transport_pid: transport_pid, + module: module + )} + end) + + assert %LvProcess{ + socket_id: ^socket_id, + root_pid: ^root_pid, + parent_pid: ^parent_pid, + pid: ^pid, + transport_pid: ^transport_pid, + module: ^module, + nested?: false, + debugger?: false, + embedded?: false + } = LvProcess.new(pid) + end + + test "sets embedded? when LiveView process is Embedded Live View" do + socket_id = "socket_id" + pid = self() + root_pid = pid + parent_pid = nil + transport_pid = :c.pid(0, 7, 0) + module = LiveDebuggerTest.TestView + + LiveDebugger.MockProcessService + |> expect(:state, fn _pid -> + {:ok, + LiveDebugger.Fakes.state( + socket_id: socket_id, + root_pid: root_pid, + parent_pid: parent_pid, + transport_pid: transport_pid, + module: module, + host_uri: :not_mounted_at_router + )} + end) + + assert %LvProcess{ + socket_id: ^socket_id, + root_pid: ^root_pid, + parent_pid: ^parent_pid, + pid: ^pid, + transport_pid: ^transport_pid, + module: ^module, + nested?: false, + debugger?: false, + embedded?: true + } = LvProcess.new(pid) + end + + test "sets debugger? when LiveView process is LiveDebugger process" do + socket_id = "socket_id" + pid = self() + root_pid = pid + parent_pid = nil + transport_pid = nil + module = LiveDebugger.TestView + + LiveDebugger.MockProcessService + |> expect(:state, fn _pid -> + {:ok, + LiveDebugger.Fakes.state( + socket_id: socket_id, + root_pid: root_pid, + parent_pid: parent_pid, + transport_pid: transport_pid, + module: module + )} + end) + + assert %LvProcess{ + socket_id: ^socket_id, + root_pid: ^root_pid, + parent_pid: ^parent_pid, + pid: ^pid, + transport_pid: ^transport_pid, + module: ^module, + nested?: false, + debugger?: true, + embedded?: false + } = LvProcess.new(pid) + end + + test "sets nested? when LiveView process is a Nested Live View" do + socket_id = "socket_id" + pid = :c.pid(0, 0, 1) + parent_pid = :c.pid(0, 0, 0) + root_pid = parent_pid + transport_pid = nil + module = LiveDebuggerTest.TestView + + LiveDebugger.MockProcessService + |> expect(:state, fn _pid -> + {:ok, + LiveDebugger.Fakes.state( + socket_id: socket_id, + root_pid: root_pid, + parent_pid: parent_pid, + transport_pid: transport_pid, + module: module + )} + end) + + assert %LvProcess{ + socket_id: ^socket_id, + root_pid: ^root_pid, + parent_pid: ^parent_pid, + pid: ^pid, + transport_pid: ^transport_pid, + module: ^module, + nested?: true, + debugger?: false, + embedded?: false + } = LvProcess.new(pid) + end + end + + describe "parent/1" do + test "returns the parent process of the given LvProcess" do + socket_id = "socket_id" + pid = :c.pid(0, 0, 1) + parent_pid = :c.pid(0, 0, 0) + root_pid = parent_pid + transport_pid = nil + module = LiveDebuggerTest.TestView + + LiveDebugger.MockProcessService + |> expect(:state, fn ^parent_pid -> + {:ok, + LiveDebugger.Fakes.state( + socket_id: socket_id, + root_pid: root_pid, + parent_pid: nil, + transport_pid: transport_pid, + module: module + )} + end) + + lv_process = %LvProcess{ + socket_id: socket_id, + root_pid: root_pid, + parent_pid: parent_pid, + pid: pid, + transport_pid: transport_pid, + module: module, + nested?: true, + debugger?: false, + embedded?: false + } + + assert %LvProcess{ + pid: ^parent_pid + } = LvProcess.parent(lv_process) + end + + test "returns nil if no parent" do + lv_process = %LvProcess{ + socket_id: "socket_id", + root_pid: self(), + parent_pid: nil, + pid: self(), + transport_pid: nil, + module: LiveDebuggerTest.TestView, + nested?: false, + debugger?: false, + embedded?: false + } + + assert LvProcess.parent(lv_process) == nil + end + end +end diff --git a/test/structs/trace_display_test.exs b/test/structs/trace_display_test.exs new file mode 100644 index 000000000..ab95602b6 --- /dev/null +++ b/test/structs/trace_display_test.exs @@ -0,0 +1,39 @@ +defmodule LiveDebugger.Structs.TraceDisplayTest do + use ExUnit.Case, async: true + + alias LiveDebugger.Structs.TraceDisplay + alias LiveDebugger.Structs.Trace + + @trace %Trace{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + arity: 3, + args: ["event", %{"key" => "value"}, %{}], + socket_id: "socket_id", + transport_pid: self(), + pid: self(), + cid: nil, + timestamp: System.system_time(:millisecond) + } + + test "from_trace/1 creates a TraceDisplay struct" do + trace = @trace + trace_display = TraceDisplay.from_trace(trace) + + assert %TraceDisplay{id: 1, trace: ^trace, render_body?: false} = trace_display + end + + test "render_body/1 sets render_body? to true" do + trace_display = %TraceDisplay{ + id: 1, + trace: @trace, + render_body?: false, + counter: 0 + } + + updated_trace_display = TraceDisplay.render_body(trace_display) + + assert %TraceDisplay{render_body?: true} = updated_trace_display + end +end diff --git a/test/structs/trace_test.exs b/test/structs/trace_test.exs new file mode 100644 index 000000000..a5f657b10 --- /dev/null +++ b/test/structs/trace_test.exs @@ -0,0 +1,215 @@ +defmodule LiveDebugger.Structs.TraceTest do + use ExUnit.Case, async: true + + alias LiveDebugger.Structs.Trace + + describe "new/6" do + test "creates a new Trace struct with the given parameters" do + id = 1 + module = LiveDebuggerTest.TestView + function = :handle_event + args = ["event", %{"key" => "value"}, %{}] + pid = :c.pid(0, 0, 1) + + assert %Trace{ + id: ^id, + module: ^module, + function: ^function, + args: ^args, + pid: ^pid + } = Trace.new(id, module, function, args, pid) + end + + test "adds timestamp when created" do + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %{}], + pid: :c.pid(0, 0, 1) + } + + trace = call_trace_new_with_map(trace_map) + + timestamp = :os.system_time(:microsecond) + + assert is_integer(trace.timestamp) + assert abs(trace.timestamp - timestamp) < 200 + end + + test "properly gets transport_pid and socket_id from live view socket" do + pid = :c.pid(0, 0, 1) + transport_pid = :c.pid(0, 0, 2) + socket_id = "socket_id" + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: [ + "event", + %{"key" => "value"}, + %Phoenix.LiveView.Socket{transport_pid: transport_pid, id: socket_id} + ], + pid: pid + } + + assert %Trace{ + transport_pid: ^transport_pid, + socket_id: ^socket_id + } = call_trace_new_with_map(trace_map) + end + + test "properly gets transport_pid and socket_id from map" do + pid = :c.pid(0, 0, 1) + transport_pid = :c.pid(0, 0, 2) + socket_id = "socket_id" + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: [ + "event", + %{"key" => "value"}, + %{socket: %Phoenix.LiveView.Socket{transport_pid: transport_pid, id: socket_id}} + ], + pid: pid + } + + assert %Trace{ + transport_pid: ^transport_pid, + socket_id: ^socket_id + } = call_trace_new_with_map(trace_map) + end + + test "properly gets cid from myself in args" do + pid = :c.pid(0, 0, 1) + cid = %Phoenix.LiveComponent.CID{cid: 1} + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %Phoenix.LiveView.Socket{assigns: %{myself: cid}}], + pid: pid + } + + assert %Trace{cid: ^cid} = call_trace_new_with_map(trace_map) + end + + test "properly gets cid from assigns in args" do + pid = :c.pid(0, 0, 1) + cid = %Phoenix.LiveComponent.CID{cid: 1} + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %{myself: cid}], + pid: pid + } + + assert %Trace{cid: ^cid} = call_trace_new_with_map(trace_map) + end + end + + describe "node_id/1" do + test "returns the pid if cid is nil" do + pid = :c.pid(0, 0, 1) + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %{}], + pid: pid + } + + trace = call_trace_new_with_map(trace_map) + + assert Trace.node_id(trace) == pid + end + + test "returns the cid if it is not nil" do + cid = %Phoenix.LiveComponent.CID{cid: 1} + + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %Phoenix.LiveView.Socket{assigns: %{myself: cid}}], + pid: :c.pid(0, 0, 1), + socket_id: "socket_id" + } + + trace = call_trace_new_with_map(trace_map) + + assert Trace.node_id(trace) == cid + end + end + + describe "live_component_delete?/1" do + test "returns true if the trace is a delete live component trace" do + cid = %Phoenix.LiveComponent.CID{cid: 1} + + trace_map = %{ + id: 1, + module: Phoenix.LiveView.Diff, + function: :delete_component, + args: [cid, {nil, nil, []}], + pid: :c.pid(0, 0, 1), + socket_id: "socket_id" + } + + trace = call_trace_new_with_map(trace_map) + + assert Trace.live_component_delete?(trace) + end + + test "returns false if the trace is not a delete live component trace" do + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %{}], + pid: :c.pid(0, 0, 1) + } + + trace = call_trace_new_with_map(trace_map) + + refute Trace.live_component_delete?(trace) + end + end + + test "callback_name/1 returns callback with arity" do + trace_map = %{ + id: 1, + module: LiveDebuggerTest.TestView, + function: :handle_event, + args: ["event", %{"key" => "value"}, %{}], + pid: :c.pid(0, 0, 1) + } + + trace = call_trace_new_with_map(trace_map) + + assert Trace.callback_name(trace) == "handle_event/3" + end + + defp call_trace_new_with_map( + %{ + id: id, + module: module, + function: function, + args: args, + pid: pid + } = map + ) do + opts = + map + |> Map.drop([:id, :module, :function, :args, :pid]) + |> Enum.into([]) + + Trace.new(id, module, function, args, pid, opts) + end +end diff --git a/test/structs/tree_node_test.exs b/test/structs/tree_node_test.exs index 4d82a8da5..a342654fb 100644 --- a/test/structs/tree_node_test.exs +++ b/test/structs/tree_node_test.exs @@ -5,85 +5,204 @@ defmodule LiveDebugger.Structs.TreeNodeTest do @cid_1 %Phoenix.LiveComponent.CID{cid: 1} - test "add_child/2" do - parent = %TreeNode.LiveView{children: []} - child = %TreeNode.LiveComponent{cid: @cid_1} + describe "id/1" do + test "returns pid for LiveView" do + pid = :c.pid(0, 0, 0) + node = %TreeNode.LiveView{pid: pid} - assert TreeNode.add_child(parent, child) == %TreeNode.LiveView{children: [child]} + assert TreeNode.id(node) == pid + end + + test "returns cid for LiveComponent" do + node = %TreeNode.LiveComponent{cid: @cid_1} + + assert TreeNode.id(node) == @cid_1 + end end - test "get_child/2 with cid" do - cid = @cid_1 + describe "type/1" do + test "returns :live_view for LiveView" do + node = %TreeNode.LiveView{} + + assert TreeNode.type(node) == :live_view + end - parent = %TreeNode.LiveView{ - children: [%TreeNode.LiveComponent{cid: cid}] - } + test "returns :live_component for LiveComponent" do + node = %TreeNode.LiveComponent{} - assert [TreeNode.get_child(parent, cid)] == parent.children + assert TreeNode.type(node) == :live_component + end + + test "returns :live_view for pid" do + pid = :c.pid(0, 0, 0) + + assert TreeNode.type(pid) == :live_view + end + + test "returns :live_component for cid" do + assert TreeNode.type(@cid_1) == :live_component + end end - test "get_child/2 with pid" do - pid = :c.pid(0, 0, 0) - parent = %TreeNode.LiveView{children: [%TreeNode.LiveView{pid: pid}]} + describe "display_id/1" do + test "returns string representation of pid" do + pid = :c.pid(0, 0, 0) + node = %TreeNode.LiveView{pid: pid} + + assert TreeNode.display_id(node) == "0.0.0" + end + + test "returns string representation of cid" do + node = %TreeNode.LiveComponent{cid: @cid_1} - assert parent.children == [TreeNode.get_child(parent, pid)] + assert TreeNode.display_id(node) == "1" + end end - test "live_view_node/1 with valid channel_state" do - pid = :c.pid(0, 0, 0) + describe "id_from_string/1" do + test "parses pid from string" do + pid = :c.pid(0, 0, 0) + id = "0.0.0" - state = %{ - socket: %{ - id: 1, - root_pid: pid, - view: :view, - assigns: %{} - } - } + assert TreeNode.id_from_string(id) == {:ok, pid} + end + + test "parses cid from string" do + id = "1" + + assert TreeNode.id_from_string(id) == {:ok, @cid_1} + end - assert {:ok, %TreeNode.LiveView{id: 1, pid: ^pid, module: :view, assigns: %{}, children: []}} = - TreeNode.live_view_node(state) + test "returns :error for invalid string" do + assert TreeNode.id_from_string("invalid") == :error + end end - test "live_view_node/1 with invalid view" do - assert TreeNode.live_view_node(%{}) == {:error, :invalid_channel_view} + test "add_child/2 adds child to the parent" do + parent = %TreeNode.LiveView{children: []} + child = %TreeNode.LiveComponent{cid: @cid_1} + + assert TreeNode.add_child(parent, child) == %TreeNode.LiveView{children: [child]} end - test "live_component_node/2 with valid channel_state and existing live_component" do - channel_state = %{components: {%{1 => {:module, "component-id", %{}, nil, nil}}, nil, nil}} + describe "get_child/2" do + test "returns nil for non-existing TreeNode.LiveView child" do + parent = %TreeNode.LiveView{ + children: [ + %TreeNode.LiveComponent{cid: @cid_1} + ] + } - cid = @cid_1 + child_id = :c.pid(0, 0, 0) - assert {:ok, %TreeNode.LiveComponent{cid: ^cid, module: :module}} = - TreeNode.live_component_node(channel_state, cid) + assert TreeNode.get_child(parent, child_id) == nil + end + + test "returns nil for non-existing TreeNode.LiveComponent child" do + parent = %TreeNode.LiveView{ + children: [ + %TreeNode.LiveView{pid: :c.pid(0, 0, 0)} + ] + } + + child_id = @cid_1 + + assert TreeNode.get_child(parent, child_id) == nil + end + + test "returns child for existing pid" do + pid = :c.pid(0, 0, 0) + parent = %TreeNode.LiveView{children: [%TreeNode.LiveView{pid: pid}]} + + assert TreeNode.get_child(parent, pid) == %TreeNode.LiveView{pid: pid} + end + + test "returns child for existing cid" do + cid = @cid_1 + parent = %TreeNode.LiveView{children: [%TreeNode.LiveComponent{cid: cid}]} + + assert TreeNode.get_child(parent, cid) == %TreeNode.LiveComponent{cid: cid} + end end - test "live_component_node/2 with valid channel_state and non-existing live_component" do - channel_state = %{components: {%{1 => {:module, "component-id", %{}, nil, nil}}, nil, nil}} + describe "live_view_node/1" do + test "returns TreeNode.LiveView for a valid channel_state" do + pid = :c.pid(0, 0, 0) + + state = %{ + socket: %{ + id: 1, + root_pid: pid, + view: :view, + assigns: %{} + } + } + + assert {:ok, + %TreeNode.LiveView{id: 1, pid: ^pid, module: :view, assigns: %{}, children: []}} = + TreeNode.live_view_node(state) + end - assert {:ok, nil} = - TreeNode.live_component_node(channel_state, %Phoenix.LiveComponent.CID{cid: 2}) + test "returns error for invalid channel_state" do + assert TreeNode.live_view_node(%{}) == {:error, :invalid_channel_view} + end end - test "live_component_node/2 with invalid channel_state" do - component = %{} + describe "live_component_node/2" do + test "returns TreeNode.LiveComponent for a valid channel_state" do + channel_state = %{ + components: {%{1 => {:module, "component-id", %{}, nil, nil}}, nil, nil} + } + + cid = @cid_1 - assert {:error, :invalid_channel_state} = - TreeNode.live_component_node(component, @cid_1) + assert {:ok, %TreeNode.LiveComponent{cid: ^cid, module: :module}} = + TreeNode.live_component_node(channel_state, cid) + end + + test "returns nil for non-existing live_component" do + channel_state = %{ + components: {%{1 => {:module, "component-id", %{}, nil, nil}}, nil, nil} + } + + assert {:ok, nil} = + TreeNode.live_component_node(channel_state, %Phoenix.LiveComponent.CID{cid: 2}) + end + + test "returns error for invalid channel_state" do + component = %{} + + assert {:error, :invalid_channel_state} = + TreeNode.live_component_node(component, @cid_1) + end end - test "live_component_nodes/1 with valid channel_state" do - channel_state = %{ - components: - {%{ - 1 => {:module, "component-id-2", %{}, nil, nil}, - 2 => {:module, "component-id-2", %{}, nil, nil} - }, nil, nil} - } + describe "live_component_nodes/1" do + test "returns list of live components" do + channel_state = %{ + components: + {%{ + 1 => {:module, "component-id-1", %{}, nil, nil}, + 2 => {:module, "component-id-2", %{}, nil, nil} + }, nil, nil} + } + + assert {:ok, [%TreeNode.LiveComponent{}, %TreeNode.LiveComponent{}]} = + TreeNode.live_component_nodes(channel_state) + end + + test "returns empty list for empty channel_state" do + channel_state = %{ + components: {%{}, nil, nil} + } + + assert {:ok, []} = TreeNode.live_component_nodes(channel_state) + end - assert {:ok, live_components} = - TreeNode.live_component_nodes(channel_state) + test "returns error for invalid channel_state" do + channel_state = %{} - assert length(live_components) == 2 + assert {:error, :invalid_channel_state} = TreeNode.live_component_nodes(channel_state) + end end end diff --git a/test/support/fakes.ex b/test/support/fakes.ex index 1a6ecec84..50b31eeba 100644 --- a/test/support/fakes.ex +++ b/test/support/fakes.ex @@ -9,6 +9,7 @@ defmodule LiveDebugger.Fakes do parent_pid = Keyword.get(opts, :parent_pid, nil) transport_pid = Keyword.get(opts, :transport_pid, :c.pid(0, 7, 0)) module = Keyword.get(opts, :module, LiveDebuggerWeb.Main) + host_uri = Keyword.get(opts, :host_uri, "https://localhost:4000") %{ socket: %Phoenix.LiveView.Socket{ @@ -19,6 +20,7 @@ defmodule LiveDebugger.Fakes do root_pid: root_pid, router: LiveDebuggerDev.Router, transport_pid: transport_pid, + host_uri: host_uri, assigns: %{ assign: :value, counter: 0,