develop hub page & test it

This commit is contained in:
Victor Martinez 2024-03-29 15:55:59 +01:00
parent d382215b04
commit adba07c752
6 changed files with 540 additions and 0 deletions

View file

@ -0,0 +1,31 @@
defmodule IntisyncWeb.DeviceCardComponent do
use Phoenix.Component
import IntisyncWeb.CoreComponents
attr :id, :string, required: true
attr :device, :map, required: true
attr :disabled, :boolean, default: false
def view(assigns) do
~H"""
<div
id={@id}
class="text-zinc-600 max-w-md border-solid border-2 rounded-lg border-indigo-500 p-4 "
>
<p class="font-semibold text-2xl mb-4">
<.icon name="hero-link-solid" class="mr-1" />
<%= @device.name %>
</p>
<input
type="range"
min="0"
max="100"
name="vibration"
value={@device.vibration}
disabled={@disabled}
class="w-full h-3 cursor-pointer"
/>
</div>
"""
end
end

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title>
<%= assigns[:page_title] || "Intisync" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/hub.js"}>
</script>
</head>
<body class="bg-white antialiased">
<%= @inner_content %>
</body>
</html>

View file

@ -0,0 +1,147 @@
defmodule IntisyncWeb.HubLive do
alias Intisync.SessionPubSub
use IntisyncWeb, :live_view
alias IntisyncWeb.LiveViewMonitor
alias Intisync.SessionsSupervisor
alias Intisync.SessionServer
def mount(_params, _session, socket) do
socket =
socket
|> assign(:remote_controller_status, nil)
|> assign(:intiface_client_status, nil)
|> assign(:devices, %{})
|> assign(:session_id, nil)
{:ok, socket}
end
def handle_params(%{"id" => session_id}, _uri, socket) do
if connected?(socket),
do: handle_connected(session_id, socket),
else: {:noreply, socket}
end
defp handle_connected(session_id, socket) do
socket =
socket
|> assign(:remote_controller_status, :disconnected)
|> assign(:intiface_client_status, :disconnected)
|> assign(:session_id, session_id)
if exists_session?(session_id) do
socket = socket |> put_flash(:error, "Unauthorized") |> redirect(to: "/")
{:noreply, socket}
else
{:ok, pid} = SessionsSupervisor.start_session(session_id)
enable_subscriptions(session_id)
LiveViewMonitor.monitor(self(), __MODULE__, {pid})
{:noreply, assign(socket, :session_server_pid, pid)}
end
end
def unmount(_reason, {session_server_pid}) do
SessionsSupervisor.close_session(session_server_pid)
end
defp exists_session?(session_id) do
case SessionsSupervisor.whereis(session_id) do
nil ->
false
_ ->
true
end
end
defp enable_subscriptions(session_id) do
SessionPubSub.subscribe!(session_id, "remote", "connected")
SessionPubSub.subscribe!(session_id, "remote", "disconnected")
SessionPubSub.subscribe!(session_id, "devices", "vibrate")
end
############################
# Remote controller events #
############################
def handle_info(%{topic: "remote:connected:" <> _session_id}, socket) do
{:noreply, assign(socket, :remote_controller_status, :connected)}
end
def handle_info(%{topic: "remote:disconnected:" <> _session_id}, socket) do
{:noreply, assign(socket, :remote_controller_status, :disconnected)}
end
def handle_info(
%{
topic: "devices:vibrate:" <> _session_id,
payload: %{index: index, vibration: vibration}
},
socket
) do
devices = SessionServer.get_devices(socket.assigns.session_server_pid)
socket =
socket
|> assign(:devices, devices)
|> push_event("vibrate", %{index: index, vibration: vibration})
{:noreply, socket}
end
##########################
# Intiface Client events #
##########################
def handle_event("local_connect", %{}, socket) do
socket = push_event(socket, "local_connect", %{})
{:noreply, socket}
end
def handle_event("remote_connect", %{}, socket) do
socket = push_event(socket, "remote_connect", %{})
{:noreply, socket}
end
def handle_event("connected", %{}, socket) do
{:noreply, assign(socket, :intiface_client_status, :connected)}
end
def handle_event("disconnected", %{}, socket) do
socket =
socket
|> assign(:intiface_client_status, :disconnected)
|> assign(:devices, %{})
:ok = SessionServer.empty_devices(socket.assigns.session_server_pid)
{:noreply, socket}
end
def handle_event("connect_error", _params, socket) do
{:noreply, put_flash(socket, :error, "Failed to connect to Intiface Central. Try again!")}
end
def handle_event("device_connected", %{"index" => index, "name" => name}, socket)
when socket.assigns.intiface_client_status == :connected do
:ok =
SessionServer.device_connected(socket.assigns.session_server_pid, index, %{
name: name,
vibration: 0
})
devices = SessionServer.get_devices(socket.assigns.session_server_pid)
{:noreply, assign(socket, :devices, devices)}
end
def handle_event("device_disconnected", %{"index" => index}, socket)
when socket.assigns.intiface_client_status == :connected do
:ok = SessionServer.device_disconnected(socket.assigns.session_server_pid, index)
devices = SessionServer.get_devices(socket.assigns.session_server_pid)
{:noreply, assign(socket, :devices, devices)}
end
def handle_event("device_connected", _params, socket), do: {:noreply, socket}
def handle_event("device_disconnected", _params, socket), do: {:noreply, socket}
end

View file

@ -0,0 +1,80 @@
<header class="mb-10">
<h1 class="text-5xl mb-4 font-bold text-indigo-500">
IntiSync Hub
</h1>
<p class="text-2xl mt-2 text-zinc-600 font-semibold">
You are hosting an IntiSync session
</p>
<div id="hub-status" class="flex flex-col sm:flex-row items-left gap-3 mt-6">
<.badge
:if={@intiface_client_status == :disconnected}
id="intiface-client-disconnected-badge"
class="text-red-700 ring-red-500"
>
<.icon name="hero-signal-slash" class="mr-1.5" /> Intiface Central
</.badge>
<.badge
:if={@intiface_client_status == :connected}
id="intiface-client-connected-badge"
class="text-green-700 ring-green-500"
>
<.icon name="hero-link" class="mr-1.5" /> Intiface Central
</.badge>
<.badge
:if={@remote_controller_status == :disconnected}
id="remote-controller-disconnected-badge"
class="text-red-700 ring-red-500"
>
<.icon name="hero-signal-slash" class="mr-1.5" /> Remote controller
</.badge>
<.badge
:if={@remote_controller_status == :connected}
id="remote-controller-connected-badge"
class="text-green-700 ring-green-500"
>
<.icon name="hero-link" class="mr-1.5" /> Remote controller
</.badge>
</div>
</header>
<section
:if={@intiface_client_status == :disconnected}
id="connect-buttons"
class="flex flex-col gap-6 items-center"
>
<.button id="intiface-local-connect-button" type="button" phx-click="local_connect">
Connect to Intiface Central
</.button>
<p class="font-semibold">or</p>
<.button id="intiface-remote-connect-button" type="button" phx-click="remote_connect">
Connect to a remote Intiface Central
</.button>
</section>
<section :if={@intiface_client_status == :connected} id="connected-devices-section">
<h2 class="text-3xl mb-4 font-bold text-indigo-500">
Connected devices
</h2>
<p :if={Enum.empty?(@devices)} class="text-lg text-amber-500 font-semibold">
<.icon name="hero-signal-slash" class="mr-1.5" /> No devices connected yet
</p>
<div class="flex flex-col gap-5 mt-2">
<%= for {index, device} <- @devices do %>
<IntisyncWeb.DeviceCardComponent.view
id={"device-#{index}"}
device={device}
disabled={true}
/>
<% end %>
</div>
</section>
<div id={@session_id} phx-hook="HubHook"></div>

View file

@ -18,6 +18,10 @@ defmodule IntisyncWeb.Router do
pipe_through :browser
live "/", LobbyLive
live_session :hub, root_layout: {IntisyncWeb.Layouts, :hub_root} do
live "/sessions/:id", HubLive
end
end
# Other scopes may use custom stacks.

View file

@ -0,0 +1,260 @@
defmodule IntisyncWeb.HubLiveTest do
use IntisyncWeb.ConnCase
import Phoenix.LiveViewTest
alias Intisync.SessionsSupervisor
defp generate_session_id(_) do
%{session_id: Intisync.Puid.generate()}
end
setup [:generate_session_id]
test "A new session gets created when user access the hub with an available ID", %{
conn: conn,
session_id: session_id
} do
{:ok, _view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert SessionsSupervisor.whereis(session_id) != nil
end
test "session gets removed when hub disconnects", %{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#nav-home-btn") |> render_click() |> follow_redirect(conn)
assert SessionsSupervisor.whereis(session_id) == nil
end
test "User can't host a session that is already being hosted", %{
conn: conn,
session_id: session_id
} do
{:ok, _view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/sessions/#{session_id}")
end
test "Intiface central status is set as disconnected when creating a session", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#intiface-client-disconnected-badge") |> has_element?()
refute view |> element("#intiface-client-connected-badge") |> has_element?()
end
test "Intiface central status is set as connected when receiving a connected event from client",
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#intiface-client-connected-badge") |> has_element?()
refute view |> element("#intiface-client-disconnected-badge") |> has_element?()
end
test "Intiface central status is set back to disconnected when receiving disconnected event from client",
%{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#intiface-client-connected-badge") |> has_element?()
refute view |> element("#intiface-client-disconnected-badge") |> has_element?()
render_click(view, "disconnected", %{})
assert view |> element("#intiface-client-disconnected-badge") |> has_element?()
refute view |> element("#intiface-client-connected-badge") |> has_element?()
end
test "Remote controller status is set as disconnected when creating a session", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#remote-controller-disconnected-badge") |> has_element?()
refute view |> element("#remote-controller-connected-badge") |> has_element?()
end
test "Remote controller status is set to connected when controller connects", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
{:ok, _remote_view, _html} = live(conn, ~p"/sessions/#{session_id}/remote")
assert view |> element("#remote-controller-connected-badge") |> has_element?()
refute view |> element("#remote-controller-disconnected-badge") |> has_element?()
end
test "Remote controller status is set back to disconnected when controller disconnects", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
{:ok, remote_view, _html} = live(conn, ~p"/sessions/#{session_id}/remote")
assert remote_view |> element("#nav-home-btn") |> render_click() |> follow_redirect(conn)
assert view |> element("#remote-controller-disconnected-badge") |> has_element?()
refute view |> element("#remote-controller-connected-badge") |> has_element?()
end
test "The connect buttons section gets replaced by the devices section when intiface client connects",
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#connect-buttons") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
render_click(view, "connected", %{})
refute view |> element("#connect-buttons") |> has_element?()
assert view |> element("#connected-devices-section") |> has_element?()
end
test "The devices section gets replaced by the connect buttons section when intiface client disconnects",
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
refute view |> element("#connect-buttons") |> has_element?()
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "disconnected", %{})
assert view |> element("#connect-buttons") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
end
test "Clicking the connect button fires a connect event that gets send back to the intiface client",
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#connect-buttons") |> has_element?()
assert view |> element("#intiface-local-connect-button") |> render_click()
assert_push_event(view, "local_connect", %{})
end
test "A device connected event is ignored if intiface central is not connected", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
refute view |> element("#device-0") |> has_element?()
end
test "A device is shown when received a device connected event", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
assert view |> element("#device-0") |> render() =~ "My dummy device"
end
test "A device starts with vibration to 0 when connected", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
assert view |> element("#device-0") |> render() =~ "value=\"0\""
end
test "A device is removed from the UI when received a device disconnected event", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
assert view |> element("#device-0") |> has_element?()
render_click(view, "device_disconnected", %{index: 0})
refute view |> element("#device-0") |> has_element?()
end
test "When intiface client disconnects and connects back, the devices list is empty", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
render_click(view, "device_connected", %{index: 1, name: "My second device"})
assert view |> element("#device-0") |> has_element?()
assert view |> element("#device-1") |> has_element?()
render_click(view, "disconnected", %{})
assert view |> element("#connect-buttons") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
render_click(view, "connected", %{})
assert view |> element("#connected-devices-section") |> has_element?()
refute view |> element("#device-0") |> has_element?()
refute view |> element("#device-1") |> has_element?()
end
test "When a controller updates the vibration of a device, the hub updates the view and sends the event down to intiface client",
%{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
{:ok, remote_view, _html} = live(conn, ~p"/sessions/#{session_id}/remote")
render_click(view, "connected", %{})
render_click(view, "device_connected", %{index: 0, name: "My dummy device"})
assert view |> element("#device-0") |> render() =~ "value=\"0\""
render_change(remote_view, "vibrate_device:0", %{"vibration" => "45"})
assert view |> element("#device-0") |> render() =~ "value=\"45\""
end
end