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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-09-05 20:36:58 +02:00
co-authored by Claude Opus 5
parent 2511b7860e
commit 8eb719c4d4
2 changed files with 17 additions and 10 deletions
@@ -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)
}
}
@@ -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();