From 8eb719c4d416a447cb3400d43c8bed21fd16141e Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 5 Sep 2026 19:07:59 +0200 Subject: [PATCH] fix(player): don't render an empty sources section Removing the `ready.length === 0` early return meant a device that has not been polled yet rendered a "Sources" heading with an empty list and a "Source list unavailable" notice. That reports a problem where there is none: having read no inventory is not the same as having one we distrust. The section is hidden again when there is nothing to offer, and the availability notice is now reserved for the case it was meant for, an inventory we hold but refuse to act on. Co-Authored-By: Claude Opus 5 (1M context) --- .../soundtouchweb/browser_compatibility_test.go | 16 ++++++++++------ .../static/js/components/Sources.js | 11 +++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/service/soundtouchweb/browser_compatibility_test.go b/pkg/service/soundtouchweb/browser_compatibility_test.go index a01753bf..ac1c3253 100644 --- a/pkg/service/soundtouchweb/browser_compatibility_test.go +++ b/pkg/service/soundtouchweb/browser_compatibility_test.go @@ -965,16 +965,20 @@ func TestStaleSourcesRemainVisibleButCannotBeSelected(t *testing.T) { t.Errorf("fresh sources: enabled=%d want=%d staleIndicatorMissing=%v", enabledCount, sourceCount, staleIndicatorMissing) } - var noInventoryText, noInventoryRole string + var emptyInventoryHidden, staleEmptyAnnounced bool if err := chromedp.Run(ctx, chromedp.Evaluate(`window.renderStatus('STANDBY', '', false, 'speaker', null, [])`, nil), - chromedp.WaitVisible(`#source-inventory-status`, chromedp.ByQuery), - chromedp.Text(`#source-inventory-status`, &noInventoryText, chromedp.ByQuery), - chromedp.AttributeValue(`#source-inventory-status`, "role", &noInventoryRole, nil, chromedp.ByQuery), + chromedp.Poll(`document.querySelector('.sources-section') === null`, nil), + chromedp.Evaluate(`document.querySelector('.sources-section') === null`, &emptyInventoryHidden), + // A stale empty inventory still has something to say: we know the list + // is untrustworthy, as opposed to simply not having read one yet. + chromedp.Evaluate(`window.renderStatus('STANDBY', '', true, 'speaker', null, [])`, nil), + chromedp.WaitVisible(`#source-stale-status`, chromedp.ByQuery), + chromedp.Evaluate(`document.querySelector('#source-stale-status').textContent.trim() === 'Source list out of date'`, &staleEmptyAnnounced), ); err != nil { t.Fatalf("render missing source inventory: %v", err) } - if noInventoryText != "Source list unavailable" || noInventoryRole != "status" { - t.Errorf("missing source inventory: text=%q role=%q", noInventoryText, noInventoryRole) + if !emptyInventoryHidden || !staleEmptyAnnounced { + t.Errorf("empty inventory: hidden=%v staleAnnounced=%v", emptyInventoryHidden, staleEmptyAnnounced) } } diff --git a/pkg/service/soundtouchweb/static/js/components/Sources.js b/pkg/service/soundtouchweb/static/js/components/Sources.js index 2e4ccbc1..e75d269f 100644 --- a/pkg/service/soundtouchweb/static/js/components/Sources.js +++ b/pkg/service/soundtouchweb/static/js/components/Sources.js @@ -95,12 +95,15 @@ export function Sources({ }, [command, currentSource, currentAccount, nowPlayingRevision]); const ready = items.filter(s => s.Status === 'READY'); + // Nothing to show and nothing to say: a device that has not been polled + // yet has no inventory to call out of date. Only render once there is + // either a list to offer or a list we are refusing to act on. + if (ready.length === 0 && !sourcesStale) return null; + const availabilityMessage = sourcesStale ? 'Source list out of date' - : (ready.length === 0 ? 'Source list unavailable' : ''); - const availabilityId = sourcesStale - ? 'source-stale-status' - : (ready.length === 0 ? 'source-inventory-status' : null); + : ''; + const availabilityId = sourcesStale ? 'source-stale-status' : null; async function select(src) { clearReadbacks();