80 |
81 | This is an example of nested hooks resulting in a "ghost" element
82 | that isn't on the DOM, and is never cleaned up. In this specific example
83 | a timeout is used to show how the number of events being sent to the server
84 | grows exponentially.
85 |
Doing any of the following things fixes it:
86 |
87 |
Setting the `phx-hook` to use a fixed id.
88 |
Removing the `pushEvent` from the OuterHook `mounted` callback.
89 |
Deferring the pushEvent by wrapping it in a setTimeout.
90 |
91 |
92 |
93 | To prevent blowing up your computer, the page will reload after 4096 events, which takes ~12 seconds
94 |
95 |
96 | Total Event Calls: {@counter}
97 |
98 |
99 | I will disappear if the bug is not present.
100 |
101 | """
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/test/e2e/support/issues/issue_3656.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveViewTest.E2E.Issue3656Live do
2 | # https://github.com/phoenixframework/phoenix_live_view/issues/3656
3 |
4 | use Phoenix.LiveView
5 |
6 | def mount(_params, _session, socket) do
7 | {:ok, socket}
8 | end
9 |
10 | def render(assigns) do
11 | ~H"""
12 |
31 |
32 | {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky,
33 | id: "sticky",
34 | sticky: true
35 | )}
36 | """
37 | end
38 | end
39 |
40 | defmodule Phoenix.LiveViewTest.E2E.Issue3656Live.Sticky do
41 | use Phoenix.LiveView
42 |
43 | def mount(:not_mounted_at_router, _session, socket) do
44 | {:ok, socket, layout: false}
45 | end
46 |
47 | def render(assigns) do
48 | ~H"""
49 |
52 | """
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/test/e2e/support/issues/issue_3658.ex:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveViewTest.E2E.Issue3658Live do
2 | # https://github.com/phoenixframework/phoenix_live_view/issues/3658
3 |
4 | use Phoenix.LiveView
5 |
6 | def mount(_params, _session, socket) do
7 | {:ok, socket}
8 | end
9 |
10 | def render(assigns) do
11 | ~H"""
12 | <.link navigate="/issues/3658?navigated=true">Link 1
13 |
14 | {live_render(@socket, Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky,
15 | id: "sticky",
16 | sticky: true
17 | )}
18 | """
19 | end
20 | end
21 |
22 | defmodule Phoenix.LiveViewTest.E2E.Issue3658Live.Sticky do
23 | use Phoenix.LiveView
24 |
25 | def mount(:not_mounted_at_router, _session, socket) do
26 | {:ok, socket, layout: false}
27 | end
28 |
29 | def render(assigns) do
30 | ~H"""
31 |
32 | """
33 | end
34 | end
35 |
36 | describe "list_liveviews/0" do
37 | test "returns a list of all currently connected LiveView processes" do
38 | conn = Plug.Test.conn(:get, "/")
39 | {:ok, view, _} = live_isolated(conn, TestLV)
40 | live_views = Debug.list_liveviews()
41 |
42 | assert is_list(live_views)
43 | assert lv = Enum.find(live_views, fn lv -> lv.pid == view.pid end)
44 | assert lv.view == TestLV
45 | assert lv.transport_pid
46 | assert lv.topic
47 | end
48 | end
49 |
50 | describe "liveview_process?/1" do
51 | test "returns true if the given pid is a LiveView process" do
52 | conn = Plug.Test.conn(:get, "/")
53 | {:ok, view, _} = live_isolated(conn, TestLV)
54 | assert Debug.liveview_process?(view.pid)
55 | end
56 | end
57 |
58 | describe "socket/1" do
59 | test "returns the socket of the given LiveView process" do
60 | conn = Plug.Test.conn(:get, "/")
61 | {:ok, view, _} = live_isolated(conn, TestLV)
62 | assert {:ok, socket} = Debug.socket(view.pid)
63 | assert socket.assigns.hello == :world
64 | end
65 |
66 | test "returns an error if the given pid is not a LiveView process" do
67 | defmodule NotALiveView do
68 | use GenServer
69 |
70 | def start_link(opts) do
71 | GenServer.start_link(__MODULE__, opts)
72 | end
73 |
74 | def init(opts) do
75 | {:ok, opts}
76 | end
77 | end
78 |
79 | pid = start_supervised!(NotALiveView)
80 | assert {:error, :not_alive_or_not_a_liveview} = Debug.socket(pid)
81 | end
82 | end
83 |
84 | describe "live_components/1" do
85 | test "returns a list of all LiveComponents rendered in the given LiveView" do
86 | conn = Plug.Test.conn(:get, "/")
87 | {:ok, view, _} = live_isolated(conn, TestLV)
88 |
89 | assert {:ok, [%{id: "component-1", module: TestLV.Component}]} =
90 | Debug.live_components(view.pid)
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/test/phoenix_live_view/integrations/assigns_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveView.AssignsTest do
2 | use ExUnit.Case, async: true
3 | import Plug.Conn
4 | import Phoenix.ConnTest
5 |
6 | import Phoenix.LiveViewTest
7 | alias Phoenix.LiveViewTest.Support.Endpoint
8 |
9 | @endpoint Endpoint
10 |
11 | setup do
12 | {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}
13 | end
14 |
15 | describe "assign_new" do
16 | test "uses conn.assigns on static render then fetches on connected mount", %{conn: conn} do
17 | user = %{name: "user-from-conn", id: 123}
18 |
19 | conn =
20 | conn
21 | |> Plug.Conn.assign(:current_user, user)
22 | |> Plug.Conn.put_session(:user_id, user.id)
23 | |> get("/root")
24 |
25 | assert html_response(conn, 200) =~ "root name: user-from-conn"
26 | assert html_response(conn, 200) =~ "child static name: user-from-conn"
27 |
28 | {:ok, _, connected_html} = live(conn)
29 | assert connected_html =~ "root name: user-from-root"
30 | assert connected_html =~ "child static name: user-from-root"
31 | end
32 |
33 | test "uses assign_new from parent on dynamically added child", %{conn: conn} do
34 | user = %{name: "user-from-conn", id: 123}
35 |
36 | {:ok, view, _html} =
37 | conn
38 | |> Plug.Conn.assign(:current_user, user)
39 | |> Plug.Conn.put_session(:user_id, user.id)
40 | |> live("/root")
41 |
42 | assert render(view) =~ "child static name: user-from-root"
43 | refute render(view) =~ "child dynamic name"
44 |
45 | :ok = GenServer.call(view.pid, {:dynamic_child, :dynamic})
46 |
47 | html = render(view)
48 | assert html =~ "child static name: user-from-root"
49 | assert html =~ "child dynamic name: user-from-child"
50 | end
51 | end
52 |
53 | describe "temporary assigns" do
54 | test "can be configured with mount options", %{conn: conn} do
55 | {:ok, conf_live, html} =
56 | conn
57 | |> put_session(:opts, temporary_assigns: [description: nil])
58 | |> live("/opts")
59 |
60 | assert html =~ "long description. canary"
61 | assert render(conf_live) =~ "long description. canary"
62 | socket = GenServer.call(conf_live.pid, {:exec, fn socket -> {:reply, socket, socket} end})
63 |
64 | assert socket.assigns.description == nil
65 | assert socket.assigns.canary == "canary"
66 | end
67 |
68 | test "raises with invalid options", %{conn: conn} do
69 | assert_raise ArgumentError,
70 | ~r/invalid option returned from Phoenix.LiveViewTest.Support.OptsLive.mount\/3/,
71 | fn ->
72 | conn
73 | |> put_session(:opts, oops: [:description])
74 | |> live("/opts")
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/test/phoenix_live_view/integrations/collocated_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveView.CollocatedTest do
2 | use ExUnit.Case, async: true
3 | import Phoenix.ConnTest
4 |
5 | import Phoenix.LiveViewTest
6 | alias Phoenix.LiveViewTest.Support.{Endpoint, CollocatedLive, CollocatedComponent}
7 |
8 | @endpoint Endpoint
9 |
10 | test "supports collocated views" do
11 | {:ok, view, html} = live_isolated(build_conn(), CollocatedLive)
12 | assert html =~ "Hello collocated world from live!\n
"
13 | assert render(view) =~ "Hello collocated world from live!\n"
14 | end
15 |
16 | test "supports collocated components" do
17 | assert render_component(CollocatedComponent, world: "world") =~
18 | "Hello collocated world from component!\n"
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/phoenix_live_view/integrations/connect_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveView.ConnectTest do
2 | use ExUnit.Case, async: true
3 | import Phoenix.LiveViewTest
4 | import Phoenix.ConnTest
5 |
6 | @endpoint Phoenix.LiveViewTest.Support.Endpoint
7 |
8 | describe "connect_params" do
9 | test "can be read on mount" do
10 | {:ok, live, _html} =
11 | Phoenix.ConnTest.build_conn()
12 | |> put_connect_params(%{"connect1" => "1"})
13 | |> live("/connect")
14 |
15 | assert render(live) =~ rendered_to_string(~s|params: %{"_mounts" => 0, "connect1" => "1"}|)
16 | end
17 | end
18 |
19 | describe "connect_info" do
20 | test "can be read on mount" do
21 | {:ok, live, html} =
22 | Phoenix.ConnTest.build_conn()
23 | |> Plug.Conn.put_req_header("user-agent", "custom-client")
24 | |> Plug.Conn.put_req_header("x-foo", "bar")
25 | |> Plug.Conn.put_req_header("x-bar", "baz")
26 | |> Plug.Conn.put_req_header("tracestate", "one")
27 | |> Plug.Conn.put_req_header("traceparent", "two")
28 | |> live("/connect")
29 |
30 | assert_html = fn html ->
31 | html = String.replace(html, """, "\"")
32 | assert html =~ ~S
33 | assert html =~ ~S
34 | assert html =~ ~S
35 | assert html =~ ~S
36 | assert html =~ ~S
37 | end
38 |
39 | assert_html.(html)
40 | assert_html.(render(live))
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/phoenix_live_view/integrations/expensive_runtime_checks_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveViewTest.ExpensiveRuntimeChecksTest do
2 | # this is intentionally async: false as we change the application
3 | # environment and recompile files!
4 | use ExUnit.Case, async: false
5 |
6 | import ExUnit.CaptureIO
7 |
8 | import Phoenix.ConnTest
9 |
10 | import Phoenix.LiveViewTest
11 | alias Phoenix.LiveViewTest.Support.Endpoint
12 |
13 | @endpoint Endpoint
14 |
15 | setup do
16 | {:ok, conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), %{})}
17 | end
18 |
19 | describe "async" do
20 | for fun <- [:start_async, :assign_async] do
21 | test "#{fun} warns when accessing socket in function at runtime", %{conn: conn} do
22 | _ =
23 | capture_io(:stderr, fn ->
24 | {:ok, lv, _html} = live(conn, "/expensive-runtime-checks")
25 | render_async(lv)
26 |
27 | send(self(), {:lv, lv})
28 | end)
29 |
30 | lv =
31 | receive do
32 | {:lv, lv} -> lv
33 | end
34 |
35 | warnings =
36 | capture_io(:stderr, fn ->
37 | render_hook(lv, "expensive_#{unquote(fun)}_socket")
38 | end)
39 |
40 | assert warnings =~
41 | "you are accessing the LiveView Socket inside a function given to #{unquote(fun)}"
42 | end
43 |
44 | test "#{fun} warns when accessing assigns in function at runtime", %{conn: conn} do
45 | _ =
46 | capture_io(:stderr, fn ->
47 | {:ok, lv, _html} = live(conn, "/expensive-runtime-checks")
48 | render_async(lv)
49 |
50 | send(self(), {:lv, lv})
51 | end)
52 |
53 | lv =
54 | receive do
55 | {:lv, lv} -> lv
56 | end
57 |
58 | warnings =
59 | capture_io(:stderr, fn ->
60 | render_hook(lv, "expensive_#{unquote(fun)}_assigns")
61 | end)
62 |
63 | assert warnings =~
64 | "you are accessing an assigns map inside a function given to #{unquote(fun)}"
65 | end
66 |
67 | test "#{fun} does not warns when doing it the right way", %{conn: conn} do
68 | _ =
69 | capture_io(:stderr, fn ->
70 | {:ok, lv, _html} = live(conn, "/expensive-runtime-checks")
71 | render_async(lv)
72 |
73 | send(self(), {:lv, lv})
74 | end)
75 |
76 | lv =
77 | receive do
78 | {:lv, lv} -> lv
79 | end
80 |
81 | warnings =
82 | capture_io(:stderr, fn ->
83 | render_hook(lv, "good_#{unquote(fun)}")
84 | end)
85 |
86 | assert warnings == ""
87 | end
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/test/phoenix_live_view/integrations/layout_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Phoenix.LiveView.LayoutTest do
2 | use ExUnit.Case, async: true
3 | import Phoenix.ConnTest
4 |
5 | import Phoenix.LiveViewTest
6 | alias Phoenix.LiveViewTest.Support.{Endpoint, LayoutView}
7 |
8 | @endpoint Endpoint
9 |
10 | setup config do
11 | {:ok,
12 | conn: Plug.Test.init_test_session(Phoenix.ConnTest.build_conn(), config[:session] || %{})}
13 | end
14 |
15 | test "uses dead layout from router", %{conn: conn} do
16 | assert_raise ArgumentError,
17 | ~r"no \"unknown_template\" html template defined for UnknownView",
18 | fn -> live(conn, "/bad_layout") end
19 |
20 | {:ok, _, _} = live(conn, "/layout")
21 | end
22 |
23 | test "is picked from config on use", %{conn: conn} do
24 | {:ok, view, html} = live(conn, "/layout")
25 | assert html =~ ~r|LAYOUT
]+>LIVELAYOUTSTART\-123\-The value is: 123\-LIVELAYOUTEND|
26 |
27 | assert render_click(view, :double) ==
28 | "LIVELAYOUTSTART-246-The value is: 246-LIVELAYOUTEND\n"
29 | end
30 |
31 | test "is picked from config on use on first render", %{conn: conn} do
32 | conn = get(conn, "/layout")
33 |
34 | assert html_response(conn, 200) =~
35 | ~r|LAYOUT
]+>LIVELAYOUTSTART\-123\-The value is: 123\-LIVELAYOUTEND|
36 | end
37 |
38 | @tag session: %{live_layout: {LayoutView, :live_override}}
39 | test "is picked from config on mount when given a layout", %{conn: conn} do
40 | {:ok, view, html} = live(conn, "/layout")
41 |
42 | assert html =~
43 | ~r|LAYOUT
]+>LIVEOVERRIDESTART\-123\-The value is: 123\-LIVEOVERRIDEEND|
44 |
45 | assert render_click(view, :double) ==
46 | "LIVEOVERRIDESTART-246-The value is: 246-LIVEOVERRIDEEND\n"
47 | end
48 |
49 | @tag session: %{live_layout: false}
50 | test "is picked from config on mount when given false", %{conn: conn} do
51 | {:ok, view, html} = live(conn, "/layout")
52 | assert html =~ "The value is: 123
"
53 | assert render_click(view, :double) == "The value is: 246"
54 | end
55 |
56 | test "is not picked from config on use for child live views", %{conn: conn} do
57 | assert get(conn, "/parent_layout") |> html_response(200) =~
58 | "The value is: 123
"
59 |
60 | {:ok, _view, html} = live(conn, "/parent_layout")
61 | assert html =~ "The value is: 123
"
62 | end
63 |
64 | @tag session: %{live_layout: {LayoutView, :live_override}}
65 | test "is picked from config on mount even on child live views", %{conn: conn} do
66 | assert get(conn, "/parent_layout") |> html_response(200) =~
67 | ~r|
]+>LIVEOVERRIDESTART\-123\-The value is: 123\-LIVEOVERRIDEEND|
68 |
69 | {:ok, _view, html} = live(conn, "/parent_layout")
70 |
71 | assert html =~
72 | ~r|