diff --git a/cmd/server/tests/podcast_test.go b/cmd/server/tests/podcast_test.go index 2357f035..098d967d 100644 --- a/cmd/server/tests/podcast_test.go +++ b/cmd/server/tests/podcast_test.go @@ -499,6 +499,67 @@ func TestReaderSettingsCompositeKeyHeadroom(t *testing.T) { require.JSONEq(t, `{"order":"custom"}`, string(val)) } +// ---- item 2.2: queue context source + the player's feed-mode order +// control (§10 R2) ---- + +func TestPodcastPlayerOrderControl(t *testing.T) { + setup := setupTestServer(t) + lib, _ := createPodcastsLibrary(t, setup, "test PlayerOrder Podcasts") + feed, err := setup.DB.CreatePodcastFeed(adminSeedCtx(t, setup), database.CreatePodcastFeedParams{ + LibraryID: lib.ID, + FeedUrl: "https://feed.invalid/player-order.xml", + Title: "PlayerOrder Feed", + }) + require.NoError(t, err) + ep1 := upsertTestEpisode(t, setup, feed, "po-1", "PlayerOrder One", 1) + ep2 := upsertTestEpisode(t, setup, feed, "po-2", "PlayerOrder Two", 2) + + // download_state is download-owned (write-after-success guard) — + // tests set it directly under the first-admin identity (raw-pool + // execs on a detached context hit RLS and silently write 0 rows). + adminScopeExec(t, setup, adminCtxValue(t, setup), + `UPDATE media_items SET download_state = 'downloaded', file_size = 1000 + WHERE id IN ($1, $2) AND source = 'podcast_episode'`, ep1.ID, ep2.ID) + subscribeRaw(t, setup, uuid.UUID(adminCtxValue(t, setup).Bytes), feed.ID) + + feedID := uuid.UUID(feed.ID.Bytes).String() + ep1URL := "/readers/" + uuid.UUID(ep1.ID.Bytes).String() + + // The reader page embeds the sequence config in a data-json + // ATTRIBUTE — templ HTML-escapes the quotes ("), so pins must + // match the escaped form. + esc := func(s string) string { return strings.ReplaceAll(s, `"`, """) } + + t.Run("context_carries_feed_source", func(t *testing.T) { + code, body := podcastAuthed(t, setup, setup.Token, "GET", ep1URL, "") + require.Equal(t, http.StatusOK, code) + assert.Contains(t, string(body), esc(`"source":"feed"`), "queue context must carry its owner (§10 R2)") + assert.Contains(t, string(body), esc(`"feed_id":"`+feedID+`"`)) + assert.Contains(t, string(body), esc(`"order":"native"`)) + assert.Contains(t, string(body), esc(`"episode_index":1`)) + }) + + t.Run("order_dropdown_renders_feed_mode", func(t *testing.T) { + code, body := podcastAuthed(t, setup, setup.Token, "GET", ep1URL, "") + require.Equal(t, http.StatusOK, code) + assert.Contains(t, string(body), `id="player-order-select"`, "feed-mode queue renders the player order control") + assert.Contains(t, string(body), `hx-put="/api/podcasts/feeds/`+feedID+`/order"`) + assert.Contains(t, string(body), "Reverse episode order", "quick reverse button present") + }) + + t.Run("order_override_hides_dropdown_after_switch", func(t *testing.T) { + // Switch the order the way the player does (the same endpoint; + // podcastAuthed sends JSON and the handler binds both shapes), + // then re-render: the dropdown must reflect the new value. + code, body := podcastAuthed(t, setup, setup.Token, "PUT", "/api/podcasts/feeds/"+feedID+"/order", `{"order":"reverse"}`) + require.Equal(t, http.StatusOK, code, "%s", string(body)) + code, body = podcastAuthed(t, setup, setup.Token, "GET", ep1URL, "") + require.Equal(t, http.StatusOK, code) + assert.Contains(t, string(body), esc(`"order":"reverse"`)) + assert.Contains(t, string(body), `