+
+
+
+ Process Actions
+
+
+
+
+
+
+
+
+
+
+
+ <.form
+ for={@form}
+ phx-submit={@on_action}
+ id="process-send-msg-form"
+ phx-change="process-message-form-update"
+ >
+
+
+
+
+
+
+
+ Process ID: {@id}
+
+
+ """
+ end
+
+ defp border_error(true), do: "border-red-200 focus:border-red-400 hover:border-red-300"
+ defp border_error(_false), do: "border-slate-200 focus:border-slate-400 hover:border-slate-300"
+end
diff --git a/test/mix/tasks/observer_web.install_test.exs b/test/mix/tasks/observer_web.install_test.exs
index 67ce57b..5e1c410 100644
--- a/test/mix/tasks/observer_web.install_test.exs
+++ b/test/mix/tasks/observer_web.install_test.exs
@@ -2,6 +2,7 @@ defmodule Mix.Tasks.ObserverWeb.InstallTest do
use ExUnit.Case, async: true
import Igniter.Test
+ alias Mix.Tasks.ObserverWeb.Install.Docs
test "installation adds the route the necessary setup to the router" do
test_project()
@@ -62,4 +63,56 @@ defmodule Mix.Tasks.ObserverWeb.InstallTest do
...|
""")
end
+
+ test "No Phoenix router found" do
+ response =
+ test_project()
+ |> apply_igniter!()
+ |> Igniter.compose_task("observer_web.install")
+
+ assert response.warnings == [
+ "No Phoenix router found, Phoenix Liveview is needed for Observer Web\n"
+ ]
+ end
+
+ test "No dev routes found" do
+ assert_raise CaseClauseError, fn ->
+ test_project()
+ |> Igniter.Project.Module.create_module(TestWeb.Router, """
+ use TestWeb, :router
+
+ pipeline :browser do
+ plug :accepts, ["html"]
+ plug :fetch_session
+ plug :fetch_live_flash
+ plug :put_root_layout, {DevWeb.LayoutView, :root}
+ plug :protect_from_forgery
+ plug :put_secure_browser_headers
+ end
+
+ pipeline :api do
+ plug :accepts, ["json"]
+ end
+
+ scope "/" do
+ pipe_through :browser
+
+ live_dashboard "/dashboard", metrics: testWeb.Telemetry
+ forward "/mailbox", Plug.Swoosh.MailboxPreview
+ end
+ """)
+ |> apply_igniter!()
+ |> Igniter.compose_task("observer_web.install")
+ end
+ end
+
+ test "Validate info methods" do
+ description_text = Docs.long_doc()
+ assert description_text =~ "Installs Observer Web into your Phoenix application"
+
+ assert description_text =~
+ "This task configures your Phoenix application to use the Observer Web dashboard"
+
+ assert description_text =~ "mix observer_web.install"
+ end
end
diff --git a/test/observer_web/web/live/apps/page_test.exs b/test/observer_web/web/live/apps/page_test.exs
index 91c4428..5ca3cb0 100644
--- a/test/observer_web/web/live/apps/page_test.exs
+++ b/test/observer_web/web/live/apps/page_test.exs
@@ -5,6 +5,7 @@ defmodule Observer.Web.Apps.PageLiveTest do
import Mox
import Mock
+ alias Observer.Web.Helpers
alias Observer.Web.Mocks.RpcStubber
alias Observer.Web.Mocks.TelemetryStubber
@@ -344,6 +345,319 @@ defmodule Observer.Web.Apps.PageLiveTest do
assert html =~ "Process #PID<0.0.11111> is either dead or protected"
end
+ test "Select Service+Apps and Kill a process", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ {:ok, pid} =
+ Task.start(fn ->
+ # Perform a long-running operation
+ :timer.sleep(30_000)
+ "Long-running task complete!"
+ end)
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#process-kill-button")
+ |> render_click() =~ "Are you sure you want to terminate process pid:"
+
+ assert index_live
+ |> element("#confirm-button-#{Helpers.identifier_to_safe_id(id)}")
+ |> render_click() =~ "successfully terminated"
+
+ refute Process.alive?(pid)
+ end
+
+ test "Select Service+Apps and Cancel before killing a process", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ pid = Enum.random(:erlang.processes())
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#process-kill-button")
+ |> render_click() =~ "Are you sure you want to terminate process pid:"
+
+ index_live
+ |> element("#cancel-button-#{Helpers.identifier_to_safe_id(id)}")
+ |> render_click()
+
+ assert Process.alive?(pid)
+ end
+
+ test "Select Service+Apps and Garbage Collect a process", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ pid = Enum.random(:erlang.processes())
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#process-clean-memory-button")
+ |> render_click() =~ "successfully garbage collected"
+ end
+
+ test "Select Service+Apps and Toggle process Monitor", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ pid = Enum.random(:erlang.processes())
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("input[type=\"checkbox\"]")
+ |> render_click() =~ "Memory monitoring enabled for process pid:"
+
+ assert index_live
+ |> element("input[type=\"checkbox\"]")
+ |> render_click() =~ "Memory monitoring disabled for process pid:"
+ end
+
+ test "Select Service+Apps and Send a message to a process", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ {:ok, pid} =
+ Task.start_link(fn ->
+ # Perform a long-running operation
+ :timer.sleep(30_000)
+ "Long-running task complete!"
+ end)
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ index_live
+ |> element("#process-send-msg-form")
+ |> render_change(%{"process-send-message" => "{:hello, :world}"})
+
+ assert index_live
+ |> element("#process-send-msg-form")
+ |> render_submit() =~ "Message sent to process pid:"
+ end
+
+ test "Select Service+Apps and cannot send an invalid message to a process", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ pid = Enum.random(:erlang.processes())
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(pid)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#process-send-msg-form")
+ |> render_change(%{"process-send-message" => "invalid"}) =~
+ "border-red-200 focus:border-red-400 hover:border-red-300"
+ end
+
test "Select Service+Apps and select a port to request information", %{conn: conn} do
node = Node.self() |> to_string
service = Helpers.normalize_id(node)
@@ -440,6 +754,112 @@ defmodule Observer.Web.Apps.PageLiveTest do
assert html =~ "Port #Port<0.100> is either dead or protected"
end
+ test "Select Service+Apps and close a port", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ port = Port.open({:spawn, "sleep 30000"}, [:binary])
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(port)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#port-close-button")
+ |> render_click() =~ "Are you sure you want to close port id:"
+
+ index_live
+ |> element("#confirm-button-#{Helpers.identifier_to_safe_id(id)}")
+ |> render_click()
+
+ refute Port.info(port)
+ end
+
+ test "Select Service+Apps and Cancel before closing a port", %{conn: conn} do
+ node = Node.self() |> to_string
+ service = Helpers.normalize_id(node)
+ test_pid_process = self()
+
+ ObserverWeb.RpcMock
+ |> stub(:call, fn node, module, function, args, timeout ->
+ :rpc.call(node, module, function, args, timeout)
+ end)
+ |> stub(:pinfo, fn pid, information ->
+ send(test_pid_process, {:apps_page_pid, self()})
+ :rpc.pinfo(pid, information)
+ end)
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/applications")
+
+ index_live
+ |> element("#apps-multi-select-toggle-options")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-apps-kernel-add-item")
+ |> render_click()
+
+ index_live
+ |> element("#apps-multi-select-services-#{service}-add-item")
+ |> render_click()
+
+ port = Enum.random(:erlang.ports())
+
+ # Send the request 2 times to validate the path where the request
+ # was already executed.
+ id = "#{inspect(port)}"
+ series_name = "#{Node.self()}::kernel"
+
+ assert_receive {:apps_page_pid, apps_page_pid}, 1_000
+
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+ send(apps_page_pid, {"request-process", %{"id" => id, "series_name" => series_name}})
+
+ assert index_live
+ |> element("#port-close-button")
+ |> render_click() =~ "Are you sure you want to close port id:"
+
+ index_live
+ |> element("#cancel-button-#{Helpers.identifier_to_safe_id(id)}")
+ |> render_click()
+
+ assert Port.info(port)
+ end
+
test "Select Service+Apps and select a reference to request information", %{conn: conn} do
node = Node.self() |> to_string
service = Helpers.normalize_id(node)
diff --git a/test/observer_web/web/live/index.test.exs b/test/observer_web/web/live/index.test.exs
deleted file mode 100644
index 4920c04..0000000
--- a/test/observer_web/web/live/index.test.exs
+++ /dev/null
@@ -1,39 +0,0 @@
-defmodule Observer.Web.IndexLiveTest do
- use Observer.Web.ConnCase, async: false
-
- import Mox
-
- alias Observer.Web.Mocks.RpcStubber
- alias Observer.Web.Mocks.TelemetryStubber
-
- setup :verify_on_exit!
-
- test "forbidding mount using a resolver callback", %{conn: conn} do
- TelemetryStubber.defaults()
-
- assert {:error, {:redirect, redirect}} = live(conn, "/observer-limited")
- assert %{to: "/", flash: %{"error" => "Access forbidden"}} = redirect
- end
-
- test "Check iframe OFF allows root button", %{conn: conn} do
- RpcStubber.defaults()
-
- TelemetryStubber.defaults()
-
- {:ok, _index_live, html} = live(conn, "/observer/tracing")
-
- assert html =~ "Live Tracing"
- assert html =~ "ROOT"
- end
-
- test "Check iframe ON doesn't allow root button", %{conn: conn} do
- RpcStubber.defaults()
-
- TelemetryStubber.defaults()
-
- {:ok, _index_live, html} = live(conn, "/observer/tracing?iframe=true")
-
- assert html =~ "Live Tracing"
- refute html =~ "ROOT"
- end
-end
diff --git a/test/observer_web/web/live/index_test.exs b/test/observer_web/web/live/index_test.exs
new file mode 100644
index 0000000..2d41f18
--- /dev/null
+++ b/test/observer_web/web/live/index_test.exs
@@ -0,0 +1,83 @@
+defmodule Observer.Web.IndexLiveTest do
+ use Observer.Web.ConnCase, async: false
+
+ import Mox
+
+ alias Observer.Web.IndexLive, as: ObserverLive
+ alias Observer.Web.Mocks.RpcStubber
+ alias Observer.Web.Mocks.TelemetryStubber
+
+ setup :verify_on_exit!
+
+ test "forbidding mount using a resolver callback", %{conn: conn} do
+ TelemetryStubber.defaults()
+
+ assert {:error, {:redirect, redirect}} = live(conn, "/observer-limited")
+ assert %{to: "/", flash: %{"error" => "Access forbidden"}} = redirect
+ end
+
+ test "Check iframe OFF allows root button", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, _index_live, html} = live(conn, "/observer/tracing")
+
+ assert html =~ "Live Tracing"
+ assert html =~ "ROOT"
+ end
+
+ test "Check iframe ON doesn't allow root button", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, _index_live, html} = live(conn, "/observer/tracing?iframe=true")
+
+ assert html =~ "Live Tracing"
+ refute html =~ "ROOT"
+ end
+
+ test "Check Update Theme", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/tracing")
+
+ # NOTE: This test could use send() but it would have to add a delay, so it was
+ # called directly
+
+ # Get the socket from the LiveView process
+ socket = :sys.get_state(index_live.pid).socket
+
+ {:noreply, updated_socket} =
+ ObserverLive.handle_info({:update_theme, "dark"}, socket)
+
+ assert updated_socket.assigns.theme == "dark"
+
+ {:noreply, updated_socket} =
+ ObserverLive.handle_info({:update_theme, "light"}, socket)
+
+ assert updated_socket.assigns.theme == "light"
+
+ {:noreply, updated_socket} =
+ ObserverLive.handle_info({:update_theme, "system"}, socket)
+
+ assert updated_socket.assigns.theme == "system"
+ end
+
+ test "Check Clear Flash", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/tracing")
+
+ # Get the socket from the LiveView process
+ socket = :sys.get_state(index_live.pid).socket
+
+ {:noreply, _updated_socket} =
+ ObserverLive.handle_event("clear-flash", %{}, socket)
+ end
+end
diff --git a/test/observer_web/web/live/theme_component_test.exs b/test/observer_web/web/live/theme_component_test.exs
new file mode 100644
index 0000000..6b61baf
--- /dev/null
+++ b/test/observer_web/web/live/theme_component_test.exs
@@ -0,0 +1,92 @@
+defmodule Observer.Web.ThemeComponentLiveTest do
+ use Observer.Web.ConnCase, async: false
+
+ import Mox
+
+ alias Observer.Web.Mocks.RpcStubber
+ alias Observer.Web.Mocks.TelemetryStubber
+ alias Observer.Web.ThemeComponent
+
+ setup :verify_on_exit!
+
+ test "Check the rendered elements are present", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/tracing")
+
+ # Test the rendered output
+ assert index_live
+ |> element("#theme-menu-toggle")
+ |> has_element?()
+
+ assert index_live
+ |> element("#theme-menu")
+ |> has_element?()
+ end
+
+ test "Check cycle theme event", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/tracing")
+
+ # NOTE: Since there are some JS events with some back and forward from the client server
+ # the only way to test the events was calling it directly with the liveview socket
+
+ # Get the socket from the LiveView process
+ socket = :sys.get_state(index_live.pid).socket
+
+ # Call cycle theme, current light
+ socket = %{socket | assigns: %{socket.assigns | theme: "light"}}
+
+ {:noreply, _updated_socket} = ThemeComponent.handle_event("cycle-theme", %{}, socket)
+
+ assert_receive {:update_theme, "dark"}, 1_000
+
+ # Call cycle theme, current dark
+ socket = %{socket | assigns: %{socket.assigns | theme: "dark"}}
+
+ {:noreply, _updated_socket} = ThemeComponent.handle_event("cycle-theme", %{}, socket)
+
+ assert_receive {:update_theme, "system"}, 1_000
+
+ # Call cycle theme, current system
+ socket = %{socket | assigns: %{socket.assigns | theme: "system"}}
+
+ {:noreply, _updated_socket} = ThemeComponent.handle_event("cycle-theme", %{}, socket)
+
+ assert_receive {:update_theme, "light"}, 1_000
+ end
+
+ test "Check update theme event", %{conn: conn} do
+ RpcStubber.defaults()
+
+ TelemetryStubber.defaults()
+
+ {:ok, index_live, _html} = live(conn, "/observer/tracing")
+
+ # NOTE: Since there are some JS events with some back and forward from the client server
+ # the only way to test the events was calling it directly with the liveview socket
+
+ # Get the socket from the LiveView process
+ socket = :sys.get_state(index_live.pid).socket
+
+ {:noreply, _socket} =
+ ThemeComponent.handle_event("update-theme", %{"theme" => "light"}, socket)
+
+ assert_receive {:update_theme, "light"}, 1_000
+
+ {:noreply, _socket} =
+ ThemeComponent.handle_event("update-theme", %{"theme" => "dark"}, socket)
+
+ assert_receive {:update_theme, "dark"}, 1_000
+
+ {:noreply, _socket} =
+ ThemeComponent.handle_event("update-theme", %{"theme" => "system"}, socket)
+
+ assert_receive {:update_theme, "system"}, 1_000
+ end
+end