{:phoenix, “~> 1.7.1”},
{:phoenix_live_view, “~> 1.0”},
After handling client timezone information via a connect param sent during mounting I ran into problems testing it.
My solution is inspired by this and this
Running live view tests that calls get_connect_params raise error already pointed to the right direction how to handle the timezone parameter in tests.
The context
// app.js
const liveSocket = new LiveSocket("/live", Socket, {
params: {
_csrf_token: csrfToken,
// Sending the timezone from the users browser to the server to render datetime properly
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
# lib/my_app_web/router.ex
# One of the on_mount hooks in the router which will call the UserAuth.on_mount function
scope "/", MyApp do
pipe_through [:browser]
live_session :current_user,
on_mount: [
...
{MyApp.UserAuth, :put_timezone}
...
],
layout: {MyApp.Layouts, :empty} do
...
end
end
# lib/my_app_web/user_auth.ex
def on_mount(:put_timezone, _params, _session, socket) do
socket =
# Only handling the connected mount cycle
if Phoenix.LiveView.connected?(socket) do
timezone = Phoenix.LiveView.get_connect_params(socket)["timezone"]
timezone =
# validating the timezone coming from the client
with false <- is_nil(timezone),
{:ok, _} <- DateTime.from_naive(~N[0001-01-01 01:01:01], timezone) do
timezone
else
# In case no timezone is sent by the client, raise an error.
# This case indicates something is wrong with the client JS.
true ->
raise ArgumentError, "Client did not provide a timezone."
# In case the timezone is invalid, raise an error.
{:error, :time_zone_not_found} ->
raise ArgumentError, "Invalid timezone: #{inspect(timezone)}"
end
Phoenix.Component.assign(
socket,
:user_timezone,
timezone
)
else
socket
end
{:cont, socket}
end
# test/my_app_web/user_auth_test.exs
describe "on_mount: :put_timezone" do
test "mounts live_view with valid timezone present", %{conn: conn} do
assert(
{:ok, _view, _html} =
conn
|> put_connect_params(%{"timezone" => "America/Chicago"})
|> live(~p"/users/register")
)
end
test "crashes when client does not provide timezone", %{conn: conn} do
assert {{%ArgumentError{message: ~s/Client did not provide a timezone./}, _}, _} =
catch_exit(live(conn, ~p"/users/register"))
end
test "crashes with empty timezone present", %{conn: conn} do
conn = put_connect_params(conn, %{"timezone" => ""})
assert {{%ArgumentError{message: ~s/Invalid timezone: ""/}, _}, _} =
catch_exit(live(conn, ~p"/users/register"))
end
test "crashes with invalid timezone present", %{conn: conn} do
conn = put_connect_params(conn, %{"timezone" => "Imaginarynation/Capitol"})
assert {{%ArgumentError{message: ~s/Invalid timezone: "Imaginarynation\/Capitol"/}, _}, _} =
catch_exit(live(conn, ~p"/users/register"))
end
end
So far everything works like a charm but the restrictive behaviour of the UserAuth.on_mount(:put_timezone, ...)
let’s fail most test. Because no timezone is given - no surprise.
I thought I just have to assign the Phoenix.LiveViewTest.put_connect_params(%{"timezone" => "America/Chicago"})
in the MyApp.ConnCase
setup. Which I did and it solved most of the tests
# test/support/conn_case.ex
setup tags do
MyApp.DataCase.setup_sandbox(tags)
{
:ok,
conn:
Phoenix.ConnTest.build_conn()
|> Phoenix.LiveViewTest.put_connect_params(%{"timezone" => "America/Chicago"})
}
end
The problem
But I have tests which use Phoenix.ConnTest.get(conn, ~p"/")
to test redirect behaviour and I can’t make it work, so that after the call of Phoenix.ConnTest.get(conn, ~p"/")
the timezone is still available in the on_mount
.
defmodule MyApp.SomePageLiveTest do
use MyApp.ConnCase, async: false
test "some test", %{conn: conn} do
# Setup fixtures and stuff...
dbg(conn)
# > conn #=> %{
# > ...
# > private: %{
# > ...
# > :live_view_connect_params => %{"timezone" => "America/Chicago"},
# > ...
# > },
# > ...
# > }
# works fine
assert {:error, {:redirect, _}} = live(conn, ~p"/a_path_which_will_redirect")
assert {:ok, _, _} = live(conn, ~p"/a_path_which_will_not_redirect")
conn = get(conn, ~p"/a_path_which_will_not_redirect")
assert {:ok, _, _} = live(conn)
# Although the timezone is still in the `conn`...
dbg(conn)
# > conn #=> %{
# > ...
# > private: %{
# > ...
# > :live_view_connect_params => %{"timezone" => "America/Chicago"},
# > ...
# > },
# > ...
# > }
# ... it fails because the timezone is not available in the on_mount
assert {:ok, _, _} = live(conn, ~p"/users/register")
# > ...
# > ** (ArgumentError) Client did not provide a timezone.
end
I feel I don’t understand the Lifecycle of the conn
and the socket
in the Phoenix.ConnTest
and Phoenix.LiveViewTest
not good enough to solve this problem.
The questions
Is this intended behaviour?
Is it a problem of how the tests are constructed or how I handle the timezone in the on_mount
?
What’s a good way to handle this problem?