Skip to content

Commit 06aef41

Browse files
authored
Feature: create API for sending user-triggered events to LiveView process (#879)
* Basic functions * Added support for handle_event * Changed API * Fix cid * Changed name of arg * Added comment * Created mock for api * Added test * Fix credo
1 parent 0fe123b commit 06aef41

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
defmodule LiveDebugger.API.UserEvents do
2+
@moduledoc """
3+
API for sending user-triggered events and messages to LiveView processes.
4+
5+
This module provides functions to interact with LiveView processes by sending
6+
various types of messages: component updates, info messages, GenServer casts/calls,
7+
and Phoenix LiveView events.
8+
"""
9+
10+
alias LiveDebugger.Structs.LvProcess
11+
alias Phoenix.LiveComponent.CID
12+
alias LiveDebugger.CommonTypes
13+
14+
@callback send_component_update(LvProcess.t(), CommonTypes.cid(), map()) :: :ok
15+
@callback send_info_message(LvProcess.t(), term()) :: term()
16+
@callback send_genserver_cast(LvProcess.t(), term()) :: :ok
17+
@callback send_genserver_call(LvProcess.t(), term()) :: term()
18+
@callback send_lv_event(LvProcess.t(), CommonTypes.cid() | nil, String.t(), map()) :: term()
19+
20+
@doc """
21+
Sends an update to a LiveComponent.
22+
23+
This will trigger the `update_many/1` callback if defined, otherwise falls back to `update/2`.
24+
See: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-update-many
25+
"""
26+
@spec send_component_update(LvProcess.t(), CommonTypes.cid(), map()) :: :ok
27+
def send_component_update(%LvProcess{} = lv_process, %CID{} = cid, payload) do
28+
impl().send_component_update(lv_process, cid, payload)
29+
end
30+
31+
@doc """
32+
Sends an info message directly to the LiveView process.
33+
34+
The message will be handled by the `handle_info/2` callback in the LiveView.
35+
"""
36+
@spec send_info_message(LvProcess.t(), term()) :: term()
37+
def send_info_message(%LvProcess{} = lv_process, payload) do
38+
impl().send_info_message(lv_process, payload)
39+
end
40+
41+
@doc """
42+
Sends a GenServer cast to the LiveView process.
43+
44+
The message will be handled by the `handle_cast/2` callback.
45+
"""
46+
@spec send_genserver_cast(LvProcess.t(), term()) :: :ok
47+
def send_genserver_cast(%LvProcess{} = lv_process, payload) do
48+
impl().send_genserver_cast(lv_process, payload)
49+
end
50+
51+
@doc """
52+
Sends a GenServer call to the LiveView process.
53+
54+
The message will be handled by the `handle_call/3` callback.
55+
Returns the response from the LiveView process.
56+
"""
57+
@spec send_genserver_call(LvProcess.t(), term()) :: term()
58+
def send_genserver_call(%LvProcess{} = lv_process, payload) do
59+
impl().send_genserver_call(lv_process, payload)
60+
end
61+
62+
@doc """
63+
Sends a Phoenix LiveView event to the LiveView process.
64+
65+
This simulates a user-triggered event (like a button click) and will be handled
66+
by the `handle_event/3` callback in the LiveView or LiveComponent.
67+
68+
## Parameters
69+
70+
* `lv_process` - The target LiveView process
71+
* `cid` - Optional component ID (CID) if targeting a LiveComponent, `nil` for LiveView
72+
* `event` - The event name as a string
73+
* `params` - The event parameters as a map
74+
"""
75+
@spec send_lv_event(LvProcess.t(), CommonTypes.cid() | nil, String.t(), map()) :: term()
76+
def send_lv_event(%LvProcess{} = lv_process, cid \\ nil, event, params) do
77+
impl().send_lv_event(lv_process, cid, event, params)
78+
end
79+
80+
defp impl do
81+
Application.get_env(
82+
:live_debugger,
83+
:api_user_events,
84+
__MODULE__.Impl
85+
)
86+
end
87+
88+
defmodule Impl do
89+
@moduledoc false
90+
@behaviour LiveDebugger.API.UserEvents
91+
92+
alias LiveDebugger.Structs.LvProcess
93+
alias Phoenix.LiveComponent.CID
94+
95+
@impl true
96+
def send_component_update(%LvProcess{} = lv_process, %CID{} = cid, payload) do
97+
Phoenix.LiveView.send_update(lv_process.pid, cid, payload)
98+
end
99+
100+
@impl true
101+
def send_info_message(%LvProcess{} = lv_process, payload) do
102+
send(lv_process.pid, payload)
103+
end
104+
105+
@impl true
106+
def send_genserver_cast(%LvProcess{} = lv_process, payload) do
107+
GenServer.cast(lv_process.pid, payload)
108+
end
109+
110+
@impl true
111+
def send_genserver_call(%LvProcess{} = lv_process, payload) do
112+
GenServer.call(lv_process.pid, payload)
113+
end
114+
115+
@impl true
116+
def send_lv_event(%LvProcess{} = lv_process, cid, event, params) do
117+
payload = %{"event" => event, "value" => params, "type" => "debug"}
118+
payload = if is_nil(cid), do: payload, else: Map.put(payload, "cid", cid.cid)
119+
120+
message = %Phoenix.Socket.Message{
121+
topic: "lv:#{lv_process.socket_id}",
122+
event: "event",
123+
payload: payload
124+
}
125+
126+
send(lv_process.pid, message)
127+
end
128+
end
129+
end

test/api/user_events_test.exs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
defmodule LiveDebugger.API.UserEventsTest do
2+
use ExUnit.Case, async: true
3+
4+
alias LiveDebugger.API.UserEvents.Impl, as: UserEventsImpl
5+
alias LiveDebugger.Fakes
6+
alias Phoenix.LiveComponent.CID
7+
8+
defmodule TestGenServer do
9+
@moduledoc false
10+
use GenServer
11+
12+
def start_link(opts \\ []) do
13+
GenServer.start_link(__MODULE__, opts)
14+
end
15+
16+
@impl true
17+
def init(opts) do
18+
{:ok, %{test_pid: Keyword.get(opts, :test_pid)}}
19+
end
20+
21+
@impl true
22+
def handle_cast(message, state) do
23+
send(state.test_pid, {:cast_received, message})
24+
{:noreply, state}
25+
end
26+
27+
@impl true
28+
def handle_call(message, _from, state) do
29+
send(state.test_pid, {:call_received, message})
30+
{:reply, {:ok, message}, state}
31+
end
32+
33+
@impl true
34+
def handle_info(message, state) do
35+
send(state.test_pid, {:info_received, message})
36+
{:noreply, state}
37+
end
38+
end
39+
40+
describe "send_info_message/2" do
41+
test "sends a message to the process" do
42+
{:ok, server_pid} = TestGenServer.start_link(test_pid: self())
43+
lv_process = Fakes.lv_process(pid: server_pid)
44+
payload = {:test_message, :hello}
45+
46+
UserEventsImpl.send_info_message(lv_process, payload)
47+
48+
assert_receive {:info_received, {:test_message, :hello}}
49+
end
50+
end
51+
52+
describe "send_genserver_cast/2" do
53+
test "sends a GenServer cast to the LiveView process" do
54+
{:ok, server_pid} = TestGenServer.start_link(test_pid: self())
55+
lv_process = Fakes.lv_process(pid: server_pid)
56+
payload = {:some_cast, :data}
57+
58+
result = UserEventsImpl.send_genserver_cast(lv_process, payload)
59+
60+
assert result == :ok
61+
assert_receive {:cast_received, {:some_cast, :data}}
62+
end
63+
end
64+
65+
describe "send_genserver_call/2" do
66+
test "sends a GenServer call to the LiveView process and returns response" do
67+
{:ok, server_pid} = TestGenServer.start_link(test_pid: self())
68+
lv_process = Fakes.lv_process(pid: server_pid)
69+
payload = {:some_call, :data}
70+
71+
result = UserEventsImpl.send_genserver_call(lv_process, payload)
72+
73+
assert result == {:ok, {:some_call, :data}}
74+
assert_receive {:call_received, {:some_call, :data}}
75+
end
76+
end
77+
78+
describe "send_lv_event/4" do
79+
test "sends a Phoenix LiveView event without CID" do
80+
lv_process = Fakes.lv_process(pid: self(), socket_id: "phx-test-socket-123")
81+
event = "click"
82+
params = %{"id" => "button-1"}
83+
84+
UserEventsImpl.send_lv_event(lv_process, nil, event, params)
85+
86+
assert_receive %Phoenix.Socket.Message{
87+
topic: "lv:phx-test-socket-123",
88+
event: "event",
89+
payload: %{
90+
"event" => "click",
91+
"value" => %{"id" => "button-1"},
92+
"type" => "debug"
93+
}
94+
}
95+
end
96+
97+
test "sends a Phoenix LiveView event with CID targeting a LiveComponent" do
98+
lv_process = Fakes.lv_process(pid: self(), socket_id: "phx-component-socket")
99+
cid = %CID{cid: 5}
100+
event = "submit"
101+
params = %{"form" => %{"name" => "John"}}
102+
103+
UserEventsImpl.send_lv_event(lv_process, cid, event, params)
104+
105+
assert_receive %Phoenix.Socket.Message{
106+
topic: "lv:phx-component-socket",
107+
event: "event",
108+
payload: %{
109+
"event" => "submit",
110+
"value" => %{"form" => %{"name" => "John"}},
111+
"type" => "debug",
112+
"cid" => 5
113+
}
114+
}
115+
end
116+
end
117+
118+
describe "send_component_update/3" do
119+
test "calls Phoenix.LiveView.send_update with correct arguments" do
120+
{:ok, server_pid} = TestGenServer.start_link(test_pid: self())
121+
122+
lv_process = Fakes.lv_process(pid: server_pid)
123+
cid = %CID{cid: 1}
124+
payload = %{some_assign: "value"}
125+
126+
result = UserEventsImpl.send_component_update(lv_process, cid, payload)
127+
128+
assert {:phoenix, :send_update, {^cid, ^payload}} = result
129+
end
130+
end
131+
end

test/support/fakes.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,29 @@ defmodule LiveDebugger.Fakes do
123123
|> Enum.into(%{})
124124
end
125125

126+
def lv_process(opts \\ []) do
127+
pid = Keyword.get(opts, :pid, :c.pid(0, 0, 1))
128+
root_pid = Keyword.get(opts, :root_pid, pid)
129+
130+
default = [
131+
pid: pid,
132+
socket_id: Keyword.get(opts, :socket_id, "phx-test-socket"),
133+
root_pid: root_pid,
134+
parent_pid: nil,
135+
transport_pid: :c.pid(0, 7, 0),
136+
module: TestLiveView,
137+
nested?: pid != root_pid,
138+
debugger?: false,
139+
embedded?: false,
140+
alive?: true
141+
]
142+
143+
Kernel.struct!(
144+
LiveDebugger.Structs.LvProcess,
145+
Keyword.merge(default, opts)
146+
)
147+
end
148+
126149
def socket(opts \\ []) do
127150
socket_id = Keyword.get(opts, :id, "phx-GBsi_6M7paYhySQj")
128151
socket_id = Keyword.get(opts, :socket_id, socket_id)

test/test_helper.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ else
7777
LiveDebugger.MockAPIStatesStorage
7878
)
7979

80+
Mox.defmock(LiveDebugger.MockAPIUserEvents,
81+
for: LiveDebugger.API.UserEvents
82+
)
83+
84+
Application.put_env(
85+
:live_debugger,
86+
:api_user_events,
87+
LiveDebugger.MockAPIUserEvents
88+
)
89+
8090
Mox.defmock(LiveDebugger.MockClient, for: LiveDebugger.Client)
8191
Application.put_env(:live_debugger, :client, LiveDebugger.MockClient)
8292
end

0 commit comments

Comments
 (0)