Skip to content

Commit aff3f66

Browse files
committed
add assert_will_receive test helper
Uses OTP 27's trace sessions to trace messages received by the LiveView.
1 parent f0f0845 commit aff3f66

2 files changed

Lines changed: 234 additions & 0 deletions

File tree

lib/phoenix_live_view/test/live_view_test.ex

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,6 +1817,150 @@ defmodule Phoenix.LiveViewTest do
18171817
end
18181818
end
18191819

1820+
@doc """
1821+
Asserts the LiveView process will receive a message within `timeout`.
1822+
The default `timeout` is [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html#configure/1)'s
1823+
`assert_receive_timeout` (100 ms).
1824+
1825+
This requires Erlang/OTP 27 or later.
1826+
1827+
A zero-arity setup function may be passed as the third argument. It is
1828+
invoked after tracing is enabled and before waiting for the message. To
1829+
customize the timeout, pass it as the fourth argument.
1830+
1831+
## Examples
1832+
1833+
assert_will_receive view, {:test_message, num}
1834+
assert num == 1
1835+
1836+
assert_will_receive view, {:test_message, num}, fn ->
1837+
send(view.pid, {:test_message, 1})
1838+
end
1839+
1840+
assert_will_receive view, {:test_message, num}, fn ->
1841+
send(view.pid, {:test_message, 1})
1842+
end, 1000
1843+
"""
1844+
defmacro assert_will_receive(
1845+
view,
1846+
pattern,
1847+
fun \\ quote(do: fn -> nil end),
1848+
timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout)
1849+
) do
1850+
{pattern, guard} = extract_guard(pattern)
1851+
ref = Macro.unique_var(:ref, __MODULE__)
1852+
tracer = Macro.unique_var(:tracer, __MODULE__)
1853+
received = Macro.unique_var(:received, __MODULE__)
1854+
message = Macro.unique_var(:message, __MODULE__)
1855+
generated_pattern = mark_vars_as_generated(pattern)
1856+
generated_guard = guard && mark_vars_as_generated(guard)
1857+
1858+
trace_pattern =
1859+
quote do
1860+
{^unquote(ref), unquote(generated_pattern)}
1861+
end
1862+
|> apply_guard(generated_guard)
1863+
1864+
quote do
1865+
{unquote(ref), unquote(tracer)} =
1866+
Phoenix.LiveViewTest.__start_assert_will_receive__(unquote(view))
1867+
1868+
unquote(received) =
1869+
try do
1870+
unquote(fun).()
1871+
assert_receive unquote(trace_pattern), unquote(timeout)
1872+
after
1873+
Phoenix.LiveViewTest.__stop_assert_will_receive__(unquote(ref), unquote(tracer))
1874+
end
1875+
1876+
{^unquote(ref), unquote(message)} = unquote(received)
1877+
unquote(pattern) = unquote(message)
1878+
unquote(message)
1879+
end
1880+
end
1881+
1882+
defp extract_guard({:when, _, [pattern, guard]}), do: {pattern, guard}
1883+
defp extract_guard(pattern), do: {pattern, nil}
1884+
1885+
defp apply_guard(pattern, nil), do: pattern
1886+
defp apply_guard(pattern, guard), do: {:when, [], [pattern, guard]}
1887+
1888+
defp mark_vars_as_generated(pattern) do
1889+
Macro.prewalk(pattern, fn
1890+
{name, meta, context} when is_atom(name) and is_atom(context) ->
1891+
{name, [generated: true] ++ meta, context}
1892+
1893+
other ->
1894+
other
1895+
end)
1896+
end
1897+
1898+
@doc false
1899+
def __start_assert_will_receive__(%View{pid: pid}) do
1900+
if not (Code.ensure_loaded?(:trace) and function_exported?(:trace, :session_create, 3)) do
1901+
raise "assert_will_receive requires Erlang/OTP 27 or later"
1902+
end
1903+
1904+
ref = make_ref()
1905+
parent = self()
1906+
tracer = spawn(fn -> assert_will_receive_loop(parent, ref) end)
1907+
session = :trace.session_create(:phoenix_live_view_test, tracer, [])
1908+
1909+
try do
1910+
:trace.process(session, pid, true, [:receive])
1911+
{ref, {tracer, session}}
1912+
rescue
1913+
exception ->
1914+
__stop_assert_will_receive__(ref, {tracer, session})
1915+
reraise exception, __STACKTRACE__
1916+
catch
1917+
kind, reason ->
1918+
__stop_assert_will_receive__(ref, {tracer, session})
1919+
:erlang.raise(kind, reason, __STACKTRACE__)
1920+
end
1921+
end
1922+
1923+
@doc false
1924+
def __stop_assert_will_receive__(ref, {tracer, session}) do
1925+
:trace.session_destroy(session)
1926+
1927+
monitor_ref = Process.monitor(tracer)
1928+
send(tracer, {ref, :stop})
1929+
1930+
receive do
1931+
{:DOWN, ^monitor_ref, :process, ^tracer, _reason} ->
1932+
:ok
1933+
after
1934+
5_000 ->
1935+
Process.exit(tracer, :kill)
1936+
1937+
receive do
1938+
{:DOWN, ^monitor_ref, :process, ^tracer, _reason} -> :ok
1939+
end
1940+
end
1941+
1942+
flush_assert_will_receive(ref)
1943+
end
1944+
1945+
defp assert_will_receive_loop(parent, ref) do
1946+
receive do
1947+
{:trace, _pid, :receive, message} ->
1948+
send(parent, {ref, message})
1949+
assert_will_receive_loop(parent, ref)
1950+
1951+
{^ref, :stop} ->
1952+
:ok
1953+
end
1954+
end
1955+
1956+
defp flush_assert_will_receive(ref) do
1957+
receive do
1958+
{^ref, _message} -> flush_assert_will_receive(ref)
1959+
after
1960+
0 -> :ok
1961+
end
1962+
end
1963+
18201964
@doc """
18211965
Follows the redirect from a `render_*` action or an `{:error, redirect}`
18221966
tuple.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
defmodule Phoenix.LiveView.AssertWillReceiveTest do
2+
use ExUnit.Case, async: true
3+
4+
import Phoenix.LiveViewTest
5+
alias Phoenix.LiveViewTest.Support.Endpoint
6+
7+
@endpoint Endpoint
8+
9+
setup do
10+
{:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}
11+
end
12+
13+
defmodule AssertWillReceiveLive do
14+
use Phoenix.LiveView
15+
16+
def mount(_params, _session, socket) do
17+
{:ok, socket}
18+
end
19+
20+
def handle_info({:test_message, _num}, socket) do
21+
{:noreply, socket}
22+
end
23+
24+
def render(assigns) do
25+
~H""
26+
end
27+
end
28+
29+
describe "assert_will_receive" do
30+
test "asserts the LiveView process will receive a message", %{conn: conn} do
31+
{:ok, view, _} = live_isolated(conn, AssertWillReceiveLive)
32+
33+
Task.start(fn ->
34+
Process.sleep(25)
35+
send(view.pid, {:test_message, 1})
36+
end)
37+
38+
assert_will_receive(view, {:test_message, num})
39+
assert num == 1
40+
end
41+
42+
test "runs setup function after tracing is set up", %{conn: conn} do
43+
{:ok, view, _} = live_isolated(conn, AssertWillReceiveLive)
44+
45+
assert_will_receive(view, {:test_message, num}, fn ->
46+
send(view.pid, {:test_message, 1})
47+
end)
48+
49+
assert num == 1
50+
end
51+
52+
test "supports guards", %{conn: conn} do
53+
{:ok, view, _} = live_isolated(conn, AssertWillReceiveLive)
54+
55+
assert_will_receive(
56+
view,
57+
{:test_message, num} when num > 1,
58+
fn ->
59+
send(view.pid, {:test_message, 1})
60+
send(view.pid, {:test_message, 2})
61+
end,
62+
1000
63+
)
64+
65+
assert num == 2
66+
end
67+
68+
test "stops tracing after the assertion", %{conn: conn} do
69+
{:ok, view, _} = live_isolated(conn, AssertWillReceiveLive)
70+
71+
assert_will_receive(
72+
view,
73+
{:test_message, :during},
74+
fn ->
75+
send(view.pid, {:test_message, :during})
76+
end,
77+
1000
78+
)
79+
80+
send(view.pid, {:test_message, :after})
81+
82+
receive do
83+
{ref, {:test_message, :after}} when is_reference(ref) ->
84+
flunk("expected assert_will_receive to stop tracing")
85+
after
86+
50 -> :ok
87+
end
88+
end
89+
end
90+
end

0 commit comments

Comments
 (0)