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"""

Clicks: <%= @clicks %>

Text: <%= @text %>

""" 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