defmodule AiDebug.LiveViewRecorderTest do
use ExUnit.Case, async: false
alias AiDebug.LiveViewRecorder
# Test LiveView for recording
defmodule RecordableLiveView do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, clicks: 0, text: "")}
end
def handle_event("click", _params, socket) do
{:noreply, update(socket, :clicks, &(&1 + 1))}
end
def handle_event("change", %{"value" => value}, socket) do
{:noreply, assign(socket, :text, value)}
end
def render(assigns) do
~H"""
"""
end
end
setup do
# Start a test LiveView
{:ok, pid} = start_test_liveview()
on_exit(fn -> Process.exit(pid, :normal) end)
{:ok, pid: pid}
end
describe "recording lifecycle" do
test "starts and stops recording", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
assert is_pid(recorder)
assert Process.alive?(recorder)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
assert recording.id
assert recording.status == :stopped
assert is_list(recording.events)
assert recording.duration_ms > 0
end
test "captures LiveView metadata", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
assert recording.metadata.module == RecordableLiveView
assert recording.metadata.initial_assigns.clicks == 0
assert recording.metadata.initial_assigns.text == ""
end
test "respects max_events option" do
{:ok, pid} = start_test_liveview()
{:ok, recorder} = LiveViewRecorder.start_recording(pid, max_events: 5)
# Send many events
for i <- 1..10 do
send(pid, {:phoenix_live_view, {:event, "click", %{"n" => i}, %{}}})
end
Process.sleep(100)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
assert length(recording.events) <= 5
end
end
describe "event recording" do
test "records LiveView events", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
# Send some events
send(pid, {:phoenix_live_view, {:event, "click", %{}, %{ref: make_ref()}}})
send(pid, {:phoenix_live_view, {:event, "change", %{"value" => "test"}, %{ref: make_ref()}}})
Process.sleep(100)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
events = recording.events
assert length(events) >= 2
click_event = Enum.find(events, & &1.name == "click")
assert click_event.type == :event
assert click_event.value == %{}
change_event = Enum.find(events, & &1.name == "change")
assert change_event.value == %{"value" => "test"}
end
test "includes state snapshots when enabled", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid, include_state: true)
send(pid, {:phoenix_live_view, {:event, "click", %{}, %{}}})
Process.sleep(100)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
event = List.first(recording.events)
assert Map.has_key?(event, :state_snapshot)
assert event.state_snapshot.assigns
assert event.state_snapshot.components
end
test "timestamps events correctly", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
send(pid, {:phoenix_live_view, {:event, "event1", %{}, %{}}})
Process.sleep(50)
send(pid, {:phoenix_live_view, {:event, "event2", %{}, %{}}})
Process.sleep(50)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
[event1, event2 | _] = Enum.reverse(recording.events)
assert event2.timestamp > event1.timestamp
assert event2.timestamp - event1.timestamp >= 50
end
end
describe "recording persistence" do
test "saves and loads recordings", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
send(pid, {:phoenix_live_view, {:event, "test", %{"data" => "value"}, %{}}})
Process.sleep(50)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
# Save to disk
path = "/tmp/test_recording_#{System.unique_integer()}.rec"
{:ok, ^path} = LiveViewRecorder.save_recording(recording, path)
# Load from disk
{:ok, loaded} = LiveViewRecorder.load_recording(path)
assert loaded.id == recording.id
assert length(loaded.events) == length(recording.events)
assert loaded.metadata.module == recording.metadata.module
# Cleanup
File.rm!(path)
end
end
describe "recording replay" do
test "replays recorded events" do
# Record some events
{:ok, pid1} = start_test_liveview()
{:ok, recorder} = LiveViewRecorder.start_recording(pid1)
# Simulate events
send(pid1, {:phoenix_live_view, {:event, "click", %{}, %{}}})
send(pid1, {:phoenix_live_view, {:event, "change", %{"value" => "test"}, %{}}})
Process.sleep(100)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
# Start new LiveView for replay
{:ok, pid2} = start_test_liveview()
# Replay recording
{:ok, task} = LiveViewRecorder.replay_recording(pid2, recording, skip_delays: true)
Task.await(task)
# Both LiveViews should have received the same events
# (In a real implementation, we'd verify state matches)
end
test "respects playback speed" do
{:ok, pid} = start_test_liveview()
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
send(pid, {:phoenix_live_view, {:event, "event1", %{}, %{}}})
Process.sleep(100)
send(pid, {:phoenix_live_view, {:event, "event2", %{}, %{}}})
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
# Replay at 2x speed
start_time = System.monotonic_time(:millisecond)
{:ok, task} = LiveViewRecorder.replay_recording(pid, recording, speed: 2.0)
Task.await(task)
end_time = System.monotonic_time(:millisecond)
# Should take approximately half the time
duration = end_time - start_time
assert duration < 100 # Original gap was 100ms
end
end
describe "test generation" do
test "generates test code from recording", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid)
# Record some interactions
send(pid, {:phoenix_live_view, {:event, "click", %{"selector" => "#button"}, %{}}})
send(pid, {:phoenix_live_view, {:event, "change", %{"selector" => "#input", "value" => "hello"}, %{}}})
send(pid, {:phoenix_live_view, {:event, "submit", %{"selector" => "#form", "value" => %{"name" => "test"}}, %{}}})
Process.sleep(50)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
{:ok, test_code} = LiveViewRecorder.generate_test(recording, "interaction_test")
assert test_code =~ "test \"interaction_test\""
assert test_code =~ "live(conn,"
assert test_code =~ "element(\"#button\") |> render_click()"
assert test_code =~ "element(\"#input\") |> render_change(\"hello\")"
assert test_code =~ "element(\"#form\") |> render_submit(%{\"name\" => \"test\"})"
end
end
describe "recording analysis" do
test "analyzes recorded session", %{pid: pid} do
{:ok, recorder} = LiveViewRecorder.start_recording(pid, include_state: true)
# Generate some events
for i <- 1..5 do
send(pid, {:phoenix_live_view, {:event, "click", %{"n" => i}, %{}}})
Process.sleep(10)
end
send(pid, {:phoenix_live_view, {:event, "submit", %{}, %{}}})
Process.sleep(50)
{:ok, recording} = LiveViewRecorder.stop_recording(recorder)
analysis = LiveViewRecorder.analyze_recording(recording)
assert analysis.total_events >= 6
assert analysis.duration_ms > 0
assert analysis.event_types[:event] >= 6
assert "click" in analysis.interaction_flow
assert "submit" in analysis.interaction_flow
assert analysis.performance_metrics.avg_response_time > 0
assert is_list(analysis.state_changes)
end
end
# Helper functions
defp start_test_liveview do
AiDebug.Test.MockLiveView.start_link(
view: RecordableLiveView,
assigns: %{__changed__: %{}, clicks: 0, text: ""}
)
end
end