ML-182.5: migrate blocked LiveView tests

Replace direct LiveView PID and isolated component test patterns with PhoenixTest-backed page flows while keeping narrow unwrap bridges where PhoenixTest cannot express the component interaction.

Precommit: mise run dev:precommit
This commit is contained in:
Claudio Ortolina
2026-05-17 21:24:29 +01:00
parent 09b555b356
commit 122c07836b
5 changed files with 198 additions and 225 deletions
@@ -1,9 +1,11 @@
--- ---
id: ML-182.5 id: ML-182.5
title: "Wave 5: Files with send(view.pid) and live_isolated blockers (4 files)" title: "Wave 5: Files with send(view.pid) and live_isolated blockers (4 files)"
status: To Do status: Done
assignee: [] assignee:
- Codex
created_date: "2026-05-14 22:15" created_date: "2026-05-14 22:15"
updated_date: "2026-05-17 20:18"
labels: labels:
- testing - testing
- refactoring - refactoring
@@ -35,3 +37,46 @@ Blocking patterns:
2. `live_isolated/3` — no PhoenixTest equivalent; test through parent page 2. `live_isolated/3` — no PhoenixTest equivalent; test through parent page
3. `render_change/2` with nested params — `fill_in` works for individual fields but not bulk nested changes 3. `render_change/2` with nested params — `fill_in` works for individual fields but not bulk nested changes
<!-- SECTION:DESCRIPTION:END --> <!-- SECTION:DESCRIPTION:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
Approved implementation plan:
1. Convert index PubSub tests in `collection_live/index_test.exs` and `wishlist_live/index_test.exs` from raw `LiveViewTest.live/2` plus `send(view.pid, :records_index_changed)` to PhoenixTest `visit/2` sessions plus the real `MusicLibrary.Records.broadcast_index_changed/0`. Assert the newly-created record appears on `:index` pages and remains absent while the import modal action ignores the message.
2. Convert remaining raw `live/2` import-flow tests in collection/wishlist to PhoenixTest pipelines (`visit`, `fill_in`, `click_link`, `click_button`, `assert_path`) and use DB assertions for the dynamically-created redirected record.
3. For cart format-change and Release component nested-form interactions, use PhoenixTest as the outer session and keep the smallest `unwrap/2` bridge only for the label-less/nested LiveComponent form change that PhoenixTest cannot express directly.
4. Replace `release_test.exs` `live_isolated/3` coverage with parent LiveView coverage: `CollectionLive.Show` for `show_print?: true` and `ScrobbleLive.ReleaseShow` for `show_print?: false`.
5. Convert `record_actions_test.exs` to PhoenixTest `visit/2` where practical; for the chat-count test, trigger the real Chat component `send_message` flow instead of directly sending `{Chat, :chats_changed}` to the LiveView PID.
6. Run focused tests for the four task files, then `mix format` on changed files. Update the task notes/final summary and check any acceptance-style completion evidence even though this task has no explicit acceptance criteria.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the approved PhoenixTest migration plan for the four scoped files. Direct LiveView PID messaging was replaced with real PubSub broadcasts or UI/component flows; `live_isolated/3` Release coverage was replaced with routed parent LiveViews; raw `live/2` import flows were converted to PhoenixTest session pipelines. Kept small `unwrap/2` bridges only for interactions PhoenixTest cannot express directly: cart native-select `phx-change`, Release component nested form changes, record action event dispatches, and Chat component form submit.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Converted the ML-182.5 test files away from raw LiveView PID/isolation patterns while preserving the existing behavioral coverage.
Changed:
- `collection_live/index_test.exs`: PubSub tests now use `Records.broadcast_index_changed/0`; import flows use PhoenixTest session pipelines; cart format changes keep a narrow `unwrap/2` helper for the component form change.
- `wishlist_live/index_test.exs`: PubSub and single-item import tests now use PhoenixTest-style navigation and async path assertions.
- `record_actions_test.exs`: page-level tests now mount with `visit/2`; the chat-count test triggers the real Chat component submit flow instead of `send(view.pid, ...)`.
- `components/release_test.exs`: removed the isolated host LiveView and replaced `show_print?` coverage with `CollectionLive.Show` and `ScrobbleLive.ReleaseShow` parent pages.
Validation:
- `mix format test/music_library_web/live/collection_live/index_test.exs test/music_library_web/live/wishlist_live/index_test.exs test/music_library_web/live_helpers/record_actions_test.exs test/music_library_web/components/release_test.exs`
- `mix test test/music_library_web/live/collection_live/index_test.exs test/music_library_web/live/wishlist_live/index_test.exs test/music_library_web/live_helpers/record_actions_test.exs test/music_library_web/components/release_test.exs --max-failures 5` -> 52 passed
- Blocker scan for `live_isolated`, `send(view.pid)`, raw `LiveViewTest.live`, `assert_redirect`, and `LVT` returned no matches in the scoped files.
- `git diff --check` passed.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -12,7 +12,7 @@ defmodule MusicLibraryWeb.Components.ReleaseTest do
import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.Records
import Phoenix.LiveViewTest, import Phoenix.LiveViewTest,
only: [element: 2, render: 1, render_change: 2, render_click: 1] only: [element: 2, render_change: 2, render_click: 1]
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
alias MusicLibrary.Secrets alias MusicLibrary.Secrets
@@ -289,45 +289,18 @@ defmodule MusicLibraryWeb.Components.ReleaseTest do
end end
end end
defmodule ReleaseComponentHost do
@moduledoc false
use MusicLibraryWeb, :live_view
@impl true
def mount(_params, session, socket) do
{:ok,
assign(socket,
release_id: session["release_id"],
show_print?: session["show_print?"],
timezone: "UTC"
)}
end
@impl true
def render(assigns) do
~H"""
<div>
<.live_component
id="host-release"
module={MusicLibraryWeb.Components.Release}
release_id={@release_id}
show_print?={@show_print?}
sheet_id="host-sheet"
timezone={@timezone}
/>
</div>
"""
end
end
defmodule MusicLibraryWeb.Components.ReleaseTest.ShowPrintTest do defmodule MusicLibraryWeb.Components.ReleaseTest.ShowPrintTest do
use MusicLibraryWeb.ConnCase, async: false use MusicLibraryWeb.ConnCase, async: false
import Phoenix.LiveViewTest, only: [render: 1] import MusicLibrary.Fixtures.Records
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
alias MusicBrainz.Fixtures.ReleaseGroup
alias Req.Test alias Req.Test
@rg_id ReleaseGroup.release_group_id(:marbles)
@release_id ReleaseFixtures.release_id(:marbles)
defp stub_musicbrainz_release(_) do defp stub_musicbrainz_release(_) do
Test.stub(MusicBrainz.API, fn conn -> Test.stub(MusicBrainz.API, fn conn ->
case conn.request_path do case conn.request_path do
@@ -346,31 +319,19 @@ defmodule MusicLibraryWeb.Components.ReleaseTest.ShowPrintTest do
setup [:stub_musicbrainz_release] setup [:stub_musicbrainz_release]
test "true renders Print tracklist dropdown entries", %{conn: conn} do test "true renders Print tracklist dropdown entries", %{conn: conn} do
{:ok, view, _html} = record = record()
Phoenix.LiveViewTest.live_isolated(conn, ReleaseComponentHost,
session: %{
"release_id" => ReleaseFixtures.release_id(:marbles),
"show_print?" => true
}
)
render_async(view) conn
|> visit(~p"/collection/#{record.id}")
assert render(view) =~ "Print tracklist" |> render_async()
|> assert_has("a", text: "Print tracklist")
end end
test "false hides Print tracklist dropdown entries", %{conn: conn} do test "false hides Print tracklist dropdown entries", %{conn: conn} do
{:ok, view, _html} = conn
Phoenix.LiveViewTest.live_isolated(conn, ReleaseComponentHost, |> visit(~p"/scrobble/#{@rg_id}/releases/#{@release_id}")
session: %{ |> render_async()
"release_id" => ReleaseFixtures.release_id(:marbles), |> refute_has("a", text: "Print tracklist")
"show_print?" => false
}
)
render_async(view)
refute render(view) =~ "Print tracklist"
end end
end end
end end
@@ -6,13 +6,12 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
import MusicBrainz.Fixtures.ReleaseGroup import MusicBrainz.Fixtures.ReleaseGroup
import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.Records
import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1] import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1]
import Phoenix.LiveViewTest, only: [assert_redirect: 2]
alias MusicLibrary.Assets alias MusicLibrary.Assets
alias MusicLibrary.Assets.{Image, Transform} alias MusicLibrary.Assets.{Image, Transform}
alias MusicLibrary.Records
alias MusicLibrary.Records.Record alias MusicLibrary.Records.Record
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
alias Phoenix.LiveViewTest
alias Req.Test alias Req.Test
# make it a multiple of 4 for easier calculations # make it a multiple of 4 for easier calculations
@@ -24,6 +23,14 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
%{collection: records} %{collection: records}
end end
defp change_cart_format(session, selector, params) do
unwrap(session, fn view ->
view
|> element(selector)
|> Phoenix.LiveViewTest.render_change(params)
end)
end
describe "Collection" do describe "Collection" do
setup [:fill_collection] setup [:fill_collection]
@@ -96,32 +103,27 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
describe "PubSub index_changed" do describe "PubSub index_changed" do
test "reloads stream when live_action is :index", %{conn: conn} do test "reloads stream when live_action is :index", %{conn: conn} do
{:ok, view, _html} = LiveViewTest.live(conn, ~p"/collection") session = visit(conn, ~p"/collection")
html_before = LiveViewTest.render(view)
# Create a new record behind the scenes (simulating completed background import) # Create a new record behind the scenes (simulating completed background import)
_new_record = record() new_record = record()
# Send the index_changed message directly refute_has(session, "#records-#{new_record.id}")
send(view.pid, :records_index_changed)
# The view should now include the new record Records.broadcast_index_changed()
html_after = LiveViewTest.render(view)
assert html_after != html_before assert_has(session, "#records-#{new_record.id}", timeout: 200)
end end
test "ignores message when live_action is :import (guard clause)", %{conn: conn} do test "ignores message when live_action is :import (guard clause)", %{conn: conn} do
{:ok, view, _html} = LiveViewTest.live(conn, ~p"/collection/import") session = visit(conn, ~p"/collection/import")
html_before = LiveViewTest.render(view) new_record = record()
send(view.pid, :records_index_changed) Records.broadcast_index_changed()
html_after = LiveViewTest.render(view) # The message is a no-op when the grid is behind the import modal.
refute_has(session, "#records-#{new_record.id}", timeout: 100)
# Should be identical — the message is a no-op when grid is behind modal
assert html_after == html_before
end end
end end
@@ -384,65 +386,37 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
end end
test "changes the format of a cart item", %{conn: conn} do test "changes the format of a cart item", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_release_group_search() stub_release_group_search()
[first | _] = Map.get(release_group_search_results(), "release-groups") [first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"] first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import") conn
|> visit(~p"/collection/import")
view |> fill_in("Search for a record", with: "Marillion Marbles")
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"}) |> click_link("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_change() |> change_cart_format("#cart-items form", %{"format" => "vinyl"})
|> assert_has("#musicbrainz_#{first_id} span", text: "In cart")
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("#cart-items form")
|> LVT.render_change(%{"format" => "vinyl"})
assert LVT.render(view) =~ "In cart"
end end
test "rejects change_format when the resulting pair is already in the cart", %{conn: conn} do test "rejects change_format when the resulting pair is already in the cart", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_release_group_search() stub_release_group_search()
[first | _] = Map.get(release_group_search_results(), "release-groups") [first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"] first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import") session =
conn
|> visit(~p"/collection/import")
|> fill_in("Search for a record", with: "Marillion Marbles")
|> click_link("#musicbrainz_#{first_id} a", "CD")
|> click_link("#musicbrainz_#{first_id} a", "Vinyl")
view assert_has(session, "#cart-items li", count: 2)
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view session
|> LVT.element("#musicbrainz_#{first_id} a", "CD") |> change_cart_format("#cart-items li:last-child form", %{"format" => "vinyl"})
|> LVT.render_click() |> assert_has("#cart-items li", count: 2)
view
|> LVT.element("#musicbrainz_#{first_id} a", "Vinyl")
|> LVT.render_click()
html = LVT.render(view)
cart_item_ids = Regex.scan(~r/id="cart-item-(\d+)"/, html, capture: :all_but_first)
assert length(cart_item_ids) == 2
[first_id_match | _] = cart_item_ids
[item_id] = first_id_match
view
|> LVT.element("#cart-item-#{item_id} form")
|> LVT.render_change(%{"cart_item_id" => item_id, "format" => "vinyl"})
html_after = LVT.render(view)
assert Regex.scan(~r/id="cart-item-/, html_after) |> length() == 2
end end
test "clears the cart", %{conn: conn} do test "clears the cart", %{conn: conn} do
@@ -460,36 +434,26 @@ defmodule MusicLibraryWeb.CollectionLive.IndexTest do
end end
test "imports a single cart item synchronously and navigates", %{conn: conn} do test "imports a single cart item synchronously and navigates", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_full_import() stub_full_import()
[first | _] = Map.get(release_group_search_results(), "release-groups") [first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"] first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/collection/import") session =
conn
|> visit(~p"/collection/import")
|> fill_in("Search for a record", with: "Marillion Marbles")
|> click_link("#musicbrainz_#{first_id} a", "CD")
|> click_button("Import 1 record")
|> assert_path("/collection/*", timeout: 2_000)
view [record] = MusicLibrary.Repo.all(Record)
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("button", "Import 1 record")
|> LVT.render_click()
{path, _flash} = assert_redirect(view, 2_000)
"/collection/" <> record_id = path
record = MusicLibrary.Records.get_record!(record_id)
assert record.musicbrainz_id == first_id assert record.musicbrainz_id == first_id
assert record.title == "Marbles" assert record.title == "Marbles"
assert record.format == :cd assert record.format == :cd
refute is_nil(record.purchased_at) refute is_nil(record.purchased_at)
assert_path(session, ~p"/collection/#{record}")
{:ok, resized_cover_data} = Image.resize(marbles_cover_data()) {:ok, resized_cover_data} = Image.resize(marbles_cover_data())
assets = Assets.get(record.cover_hash) assets = Assets.get(record.cover_hash)
@@ -5,11 +5,10 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
import Ecto.Query, only: [from: 2] import Ecto.Query, only: [from: 2]
import MusicBrainz.Fixtures.ReleaseGroup import MusicBrainz.Fixtures.ReleaseGroup
import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.Records
import Phoenix.LiveViewTest, only: [assert_redirect: 2]
alias MusicLibrary.Records
alias MusicLibrary.Records.Record alias MusicLibrary.Records.Record
alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup alias MusicLibrary.Worker.ImportFromMusicbrainzReleaseGroup
alias Phoenix.LiveViewTest
alias Req.Test alias Req.Test
defp fill_wishlist(_) do defp fill_wishlist(_) do
@@ -19,32 +18,27 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
describe "PubSub index_changed" do describe "PubSub index_changed" do
test "reloads stream when live_action is :index", %{conn: conn} do test "reloads stream when live_action is :index", %{conn: conn} do
{:ok, view, _html} = LiveViewTest.live(conn, ~p"/wishlist") session = visit(conn, ~p"/wishlist")
html_before = LiveViewTest.render(view)
# Create a new wishlist record behind the scenes (simulating completed background import) # Create a new wishlist record behind the scenes (simulating completed background import)
_new_record = record(%{purchased_at: nil}) new_record = record(%{purchased_at: nil})
# Send the index_changed message directly refute_has(session, "#records-#{new_record.id}")
send(view.pid, :records_index_changed)
# The view should now include the new record Records.broadcast_index_changed()
html_after = LiveViewTest.render(view)
assert html_after != html_before assert_has(session, "#records-#{new_record.id}", timeout: 200)
end end
test "ignores message when live_action is :import (guard clause)", %{conn: conn} do test "ignores message when live_action is :import (guard clause)", %{conn: conn} do
{:ok, view, _html} = LiveViewTest.live(conn, ~p"/wishlist/import") session = visit(conn, ~p"/wishlist/import")
html_before = LiveViewTest.render(view) new_record = record(%{purchased_at: nil})
send(view.pid, :records_index_changed) Records.broadcast_index_changed()
html_after = LiveViewTest.render(view) # The message is a no-op when the grid is behind the import modal.
refute_has(session, "#records-#{new_record.id}", timeout: 100)
# Should be identical — the message is a no-op when grid is behind modal
assert html_after == html_before
end end
end end
@@ -107,36 +101,26 @@ defmodule MusicLibraryWeb.WishlistLive.IndexTest do
end end
test "imports a single cart item synchronously and navigates to wishlist", %{conn: conn} do test "imports a single cart item synchronously and navigates to wishlist", %{conn: conn} do
alias Phoenix.LiveViewTest, as: LVT
stub_full_import() stub_full_import()
[first | _] = Map.get(release_group_search_results(), "release-groups") [first | _] = Map.get(release_group_search_results(), "release-groups")
first_id = first["id"] first_id = first["id"]
{:ok, view, _html} = LVT.live(conn, ~p"/wishlist/import") session =
conn
|> visit(~p"/wishlist/import")
|> fill_in("Search for a record", with: "Marillion Marbles")
|> click_link("#musicbrainz_#{first_id} a", "CD")
|> click_button("Import 1 record")
|> assert_path("/wishlist/*", timeout: 2_000)
view [record] = MusicLibrary.Repo.all(Record)
|> LVT.form("#import_form", %{"mb_query" => "Marillion Marbles"})
|> LVT.render_change()
view
|> LVT.element("#musicbrainz_#{first_id} a", "CD")
|> LVT.render_click()
view
|> LVT.element("button", "Import 1 record")
|> LVT.render_click()
{path, _flash} = assert_redirect(view, 2_000)
"/wishlist/" <> record_id = path
record = MusicLibrary.Records.get_record!(record_id)
assert record.musicbrainz_id == first_id assert record.musicbrainz_id == first_id
assert record.title == "Marbles" assert record.title == "Marbles"
assert record.format == :cd assert record.format == :cd
assert record.purchased_at == nil assert record.purchased_at == nil
assert_path(session, ~p"/wishlist/#{record}")
refute_enqueued(worker: ImportFromMusicbrainzReleaseGroup) refute_enqueued(worker: ImportFromMusicbrainzReleaseGroup)
end end
@@ -4,8 +4,6 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.Records
import Phoenix.LiveViewTest, only: [live: 2, render: 1, render_click: 3]
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
alias MusicBrainz.Fixtures.ReleaseGroup alias MusicBrainz.Fixtures.ReleaseGroup
alias MusicLibrary.Chats alias MusicLibrary.Chats
@@ -43,15 +41,45 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
end) end)
end end
defp visit_record(conn, record) do
visit(conn, ~p"/collection/#{record.id}")
end
defp trigger_record_action(session, event) do
unwrap(session, fn view ->
render_click(view, event, %{})
end)
end
defp submit_record_chat_message(session, message) do
unwrap(session, fn view ->
view
|> form("#record-chat-form", %{"message" => message})
|> Phoenix.LiveViewTest.render_submit()
end)
end
defp stub_openai_stream do
body =
"data: #{JSON.encode!(%{type: "response.output_text.delta", delta: "Hello"})}\n\n" <>
"data: #{JSON.encode!(%{type: "response.completed"})}\n\n"
Test.stub(OpenAI.API, fn conn ->
conn
|> Plug.Conn.put_resp_content_type("text/event-stream")
|> Plug.Conn.send_resp(200, body)
end)
end
describe "refresh_musicbrainz_data event" do describe "refresh_musicbrainz_data event" do
test "success path shows confirmation toast", %{conn: conn} do test "success path shows confirmation toast", %{conn: conn} do
record = record() record = record()
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "refresh_musicbrainz_data", %{}) |> visit_record(record)
|> trigger_record_action("refresh_musicbrainz_data")
assert render(view) =~ "MusicBrainz data refreshed successfully" |> assert_has("#toast-group", text: "MusicBrainz data refreshed successfully")
end end
@tag :capture_log @tag :capture_log
@@ -70,11 +98,10 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
end end
end) end)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "refresh_musicbrainz_data", %{}) |> visit_record(record)
|> trigger_record_action("refresh_musicbrainz_data")
html = render(view) |> assert_has("#toast-group", text: "Error refreshing MusicBrainz data")
assert html =~ "Error refreshing MusicBrainz data"
end end
end end
@@ -83,10 +110,11 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
record = record() record = record()
stub_musicbrainz_happy_path(record.musicbrainz_id, cover_data: raven_cover_data()) stub_musicbrainz_happy_path(record.musicbrainz_id, cover_data: raven_cover_data())
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "refresh_cover", %{}) |> visit_record(record)
|> trigger_record_action("refresh_cover")
|> assert_has("#toast-group", text: "Cover refreshed successfully")
assert render(view) =~ "Cover refreshed successfully"
refute Records.get_record!(record.id).cover_hash == record.cover_hash refute Records.get_record!(record.id).cover_hash == record.cover_hash
end end
@@ -109,10 +137,10 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
end end
end) end)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "refresh_cover", %{}) |> visit_record(record)
|> trigger_record_action("refresh_cover")
assert render(view) =~ "Error refreshing cover" |> assert_has("#toast-group", text: "Error refreshing cover")
end end
end end
@@ -121,10 +149,10 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
record = record() record = record()
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "populate_genres", %{}) |> visit_record(record)
|> trigger_record_action("populate_genres")
assert render(view) =~ "In progress - record will update automatically" |> assert_has("#toast-group", text: "In progress - record will update automatically")
assert_enqueued( assert_enqueued(
worker: MusicLibrary.Worker.PopulateGenres, worker: MusicLibrary.Worker.PopulateGenres,
@@ -138,10 +166,10 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
record = record(%{dominant_colors: []}) record = record(%{dominant_colors: []})
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "extract_colors", %{}) |> visit_record(record)
|> trigger_record_action("extract_colors")
assert render(view) =~ "Colors extracted" |> assert_has("#toast-group", text: "Colors extracted")
updated = Records.get_record!(record.id) updated = Records.get_record!(record.id)
# FakeColorExtractor returns a fixed 5-color palette # FakeColorExtractor returns a fixed 5-color palette
@@ -157,10 +185,10 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
{:ok, record} = Records.update_record(record, %{cover_hash: String.duplicate("00", 32)}) {:ok, record} = Records.update_record(record, %{cover_hash: String.duplicate("00", 32)})
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
render_click(view, "extract_colors", %{}) |> visit_record(record)
|> trigger_record_action("extract_colors")
assert render(view) =~ "Error extracting colors" |> assert_has("#toast-group", text: "Error extracting colors")
end end
end end
@@ -168,22 +196,13 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
test "re-counts chats when the Chat component broadcasts a change", %{conn: conn} do test "re-counts chats when the Chat component broadcasts a change", %{conn: conn} do
record = record() record = record()
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
stub_openai_stream()
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
|> visit_record(record)
|> submit_record_chat_message("hello")
|> assert_has("span.text-xs", text: "1", timeout: 200)
{:ok, _chat} =
Chats.create_chat_with_message(
%{entity: :record, musicbrainz_id: record.musicbrainz_id},
%{role: "user", content: "hello"}
)
# The Chat component would `send(self(), {Chat, :chats_changed})` after
# seeding a chat. We simulate that by sending the same message directly.
send(view.pid, {MusicLibraryWeb.Components.Chat, :chats_changed})
# Forcing a re-render flushes the handle_info that re-computes chat_count;
# we verify via the context function which is the authoritative count.
_ = render(view)
assert Chats.count_chats(:record, record.musicbrainz_id) == 1 assert Chats.count_chats(:record, record.musicbrainz_id) == 1
end end
end end
@@ -193,7 +212,7 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
record = record() record = record()
stub_musicbrainz_happy_path(record.musicbrainz_id) stub_musicbrainz_happy_path(record.musicbrainz_id)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") session = visit_record(conn, record)
updated_record = %{record | title: "Brand New Title"} updated_record = %{record | title: "Brand New Title"}
@@ -203,9 +222,9 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
{:update, updated_record} {:update, updated_record}
) )
html = render(view) session
assert html =~ "Brand New Title" |> assert_has("*", text: "Brand New Title", timeout: 200)
assert html =~ "Record updated in the background" |> assert_has("#toast-group", text: "Record updated in the background")
end end
end end
@@ -219,9 +238,9 @@ defmodule MusicLibraryWeb.LiveHelpers.RecordActionsTest do
{:ok, _} = Similarity.store_embedding(record.id, embedding, embedding_text) {:ok, _} = Similarity.store_embedding(record.id, embedding, embedding_text)
{:ok, view, _html} = live(conn, ~p"/collection/#{record.id}") conn
|> visit_record(record)
assert render(view) =~ "Genres: progressive rock" |> assert_has("*", text: "Genres: progressive rock")
end end
# The `{:error, :not_found}` branch is exercised implicitly by every # The `{:error, :not_found}` branch is exercised implicitly by every