Files
music_library/test/music_library_web/components/release_test.exs
T
Claudio Ortolina 0776700170 ML-142: Improve scrobble UI in the release component
- Sticky bar for easier track-based scrobbling
- Support setting custom finish time
2026-04-22 17:19:35 +01:00

290 lines
9.0 KiB
Elixir

defmodule MusicLibraryWeb.Components.ReleaseTest do
@moduledoc """
Integration tests for the Release LiveComponent driven through `CollectionLive.Show`.
Covers the UI changes introduced by ML-142: the `Finished at` picker, the sticky
selection bar, the re-enabled per-medium scrobble button, and the fact that all
three scrobble entry points now use the picker value (initialised to
`DateTime.utc_now/0` at mount, resettable via the `Now` button).
"""
use MusicLibraryWeb.ConnCase, async: false
import MusicLibrary.Fixtures.Records
import Phoenix.LiveViewTest,
only: [element: 2, render_async: 1, render_change: 2, render_click: 1]
alias MusicBrainz.Fixtures.Release, as: ReleaseFixtures
alias MusicLibrary.Secrets
@finished_at ~U[2026-03-15 21:00:00Z]
# `@sheet_form` scopes form-change events to the LiveComponent; button
# clicks are scoped by `[phx-target]` to avoid colliding with the parent
# LiveView's own scrobble_release button at `lib/music_library_web/live/collection_live/show.ex:58`.
@sheet_form "#release-with-tracks-sheet-form"
defp stub_musicbrainz_release(_) do
Req.Test.stub(MusicBrainz.API, fn conn ->
case conn.request_path do
"/ws/2/release/" <> _id ->
Req.Test.json(conn, ReleaseFixtures.release_with_media(:marbles))
_ ->
Req.Test.json(conn, %{})
end
end)
:ok
end
defp capture_lastfm_scrobble(_) do
test_pid = self()
Req.Test.stub(LastFm.API, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
params = URI.decode_query(body)
send(test_pid, {:lastfm_scrobble, params})
Req.Test.json(conn, %{"scrobbles" => %{"@attr" => %{"accepted" => 1}}})
end)
:ok
end
defp store_lastfm_session_key(_) do
Secrets.store("last_fm_session_key", "test_session_key")
:ok
end
defp release_with_tracks do
:marbles
|> ReleaseFixtures.release_with_media()
|> MusicBrainz.Release.from_api_response()
end
defp first_track_id(release),
do: release.media |> List.first() |> Map.get(:tracks) |> List.first() |> Map.get(:id)
defp second_medium_first_track_id(release),
do: release.media |> Enum.at(1) |> Map.get(:tracks) |> List.first() |> Map.get(:id)
defp latest_timestamp(params) do
params
|> Enum.filter(fn {k, _v} -> String.starts_with?(k, "timestamp[") end)
|> Enum.map(fn {_k, v} -> String.to_integer(v) end)
|> Enum.max()
end
describe "Finished at picker" do
setup [:stub_musicbrainz_release, :store_lastfm_session_key, :capture_lastfm_scrobble]
test "renders 'Finished at' label when Last.fm is connected", %{conn: conn} do
record = record()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> assert_has("div[data-part=inner-prefix]", text: "Finished at")
end
test "release scrobble uses mount-time utc_now when picker is not modified", %{conn: conn} do
record = record()
before_click = DateTime.utc_now()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element("button[phx-click=scrobble_release][phx-target]")
|> render_click()
end)
after_click = DateTime.utc_now()
assert_received {:lastfm_scrobble, params}
latest = latest_timestamp(params)
# Scrobble timestamps reflect started_at + track offsets, so the latest
# timestamp is close to the click time (within a couple of seconds).
assert latest >= DateTime.to_unix(before_click) - 2
assert latest <= DateTime.to_unix(after_click) + 2
end
test "release scrobble uses the picker value when set", %{conn: conn} do
record = record()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"finished_at" => DateTime.to_iso8601(@finished_at)}})
view
|> element("button[phx-click=scrobble_release][phx-target]")
|> render_click()
end)
assert_received {:lastfm_scrobble, params}
assert latest_timestamp(params) == DateTime.to_unix(@finished_at)
end
test "reset_to_now event resets the picker back to the current time", %{conn: conn} do
record = record()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"finished_at" => DateTime.to_iso8601(@finished_at)}})
view
|> element("button[phx-click=reset_to_now]")
|> render_click()
view
|> element("button[phx-click=scrobble_release][phx-target]")
|> render_click()
end)
now_unix = DateTime.to_unix(DateTime.utc_now())
assert_received {:lastfm_scrobble, params}
latest = latest_timestamp(params)
# After resetting, the scrobble should use `utc_now/0`, not the stale value.
assert abs(latest - now_unix) < 5
refute latest == DateTime.to_unix(@finished_at)
end
end
describe "Per-medium scrobble" do
setup [:stub_musicbrainz_release, :store_lastfm_session_key, :capture_lastfm_scrobble]
test "medium-2 scrobble fires when a track on medium-1 is selected (AC#3 regression)",
%{conn: conn} do
record = record()
release = release_with_tracks()
medium_1_track = first_track_id(release)
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"selected_tracks" => [medium_1_track]}})
view
|> element("button[phx-click=scrobble_medium][phx-value-number='2']")
|> render_click()
end)
|> assert_has("#toast-group", text: "Disc scrobbled successfully")
assert_received {:lastfm_scrobble, params}
# Medium 2's tracks were scrobbled, not the medium-1 selection.
medium_2_first_title =
release.media |> Enum.at(1) |> Map.get(:tracks) |> List.first() |> Map.get(:title)
assert params["track[0]"] == medium_2_first_title
end
test "medium scrobble uses picker value when set", %{conn: conn} do
record = record()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"finished_at" => DateTime.to_iso8601(@finished_at)}})
view
|> element("button[phx-click=scrobble_medium][phx-value-number='2']")
|> render_click()
end)
assert_received {:lastfm_scrobble, params}
assert latest_timestamp(params) == DateTime.to_unix(@finished_at)
end
end
describe "Sticky selection bar" do
setup [:stub_musicbrainz_release, :store_lastfm_session_key, :capture_lastfm_scrobble]
test "is hidden when no tracks are selected", %{conn: conn} do
record = record()
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> refute_has("button", text: "Scrobble selected")
end
test "appears with a singular count when one track is selected", %{conn: conn} do
record = record()
release = release_with_tracks()
track_id = first_track_id(release)
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"selected_tracks" => [track_id]}})
end)
|> assert_has("p", text: "1 track selected")
|> assert_has("button", text: "Scrobble selected")
end
test "shows cross-medium copy when selection spans discs", %{conn: conn} do
record = record()
release = release_with_tracks()
t1 = first_track_id(release)
t2 = second_medium_first_track_id(release)
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{"release" => %{"selected_tracks" => [t1, t2]}})
end)
|> assert_has("p", text: "2 tracks selected")
|> assert_has("span", text: "across 2 discs")
end
test "Scrobble selected uses picker value", %{conn: conn} do
record = record()
release = release_with_tracks()
track_id = first_track_id(release)
conn
|> visit(~p"/collection/#{record.id}")
|> unwrap(&render_async/1)
|> unwrap(fn view ->
view
|> element(@sheet_form)
|> render_change(%{
"release" => %{
"selected_tracks" => [track_id],
"finished_at" => DateTime.to_iso8601(@finished_at)
}
})
view
|> element("button[phx-click=scrobble_selected_tracks]")
|> render_click()
end)
assert_received {:lastfm_scrobble, params}
assert latest_timestamp(params) == DateTime.to_unix(@finished_at)
end
end
end