mirror of
https://codeberg.org/JasterV/intisync.ex.git
synced 2026-04-26 18:10:07 +00:00
develop hub page & test it
This commit is contained in:
parent
d382215b04
commit
adba07c752
6 changed files with 540 additions and 0 deletions
31
lib/intisync_web/components/device_card_component.ex
Normal file
31
lib/intisync_web/components/device_card_component.ex
Normal 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
|
||||
18
lib/intisync_web/components/layouts/hub_root.html.heex
Normal file
18
lib/intisync_web/components/layouts/hub_root.html.heex
Normal 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>
|
||||
147
lib/intisync_web/live/hub_live.ex
Normal file
147
lib/intisync_web/live/hub_live.ex
Normal 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
|
||||
80
lib/intisync_web/live/hub_live.html.heex
Normal file
80
lib/intisync_web/live/hub_live.html.heex
Normal 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>
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
260
test/intisync_web/live/hub_live_test.exs
Normal file
260
test/intisync_web/live/hub_live_test.exs
Normal 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
|
||||
Loading…
Reference in a new issue