diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index d78c3dbcc..115a3c76d 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -3820,7 +3820,15 @@ async function main() { radio.stopPlayback(); Array.from({ length: gev.viewer.dataSources.length }, (_, index) => gev.viewer.dataSources.get(index)) .find((item) => item.name === 'Radio stations').clustering.enabled = true; - radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: false, focus: false }); + // Next only carries playback once a stream is active (HAL-722), so the + // fallback scenario starts from a playing station. + radio.selectStation('00000000-0000-4000-8000-000000000001', { autoplay: true, focus: false }); + }); + await page.waitForFunction(() => window.__godsEyeView.dataManager.layers.get('radio').module.getUIState().audioState === 'playing'); + await page.evaluate(() => { + const gev = window.__godsEyeView; + const camera = gev.viewer.camera; + const radio = gev.dataManager.layers.get('radio').module; const pool = gev.styleManager._radioControls._radioTunerPool; const selectedIndex = pool.findIndex((station) => station.id === radio.getUIState().selected?.id); window.__qaRadioExpectedPrimary = pool[(selectedIndex + 1) % pool.length]; diff --git a/src/ui/radioBindings.js b/src/ui/radioBindings.js index 88756d5bc..8f48b9bc9 100644 --- a/src/ui/radioBindings.js +++ b/src/ui/radioBindings.js @@ -338,9 +338,14 @@ export function bindRadioControls() { const pool = this._radioTunerPool.length ? this._radioTunerPool : this._radioTunerStations; + // Stepping only selects until the user has pressed Play; once a stream is + // active it follows the selection so the old broadcaster does not keep + // playing under a new station label. + const { audioState } = this.radio.getUIState(); const cycled = this.radio.cycleStation(direction, { rotate, stationIds: pool.map((station) => station.id), + autoplay: ['loading', 'buffering', 'playing'].includes(audioState), origin: 'user', }); if (!cycled) { diff --git a/src/ui/radioControls.test.mjs b/src/ui/radioControls.test.mjs index b99c96a18..83bbafd7c 100644 --- a/src/ui/radioControls.test.mjs +++ b/src/ui/radioControls.test.mjs @@ -40,8 +40,11 @@ function fixture() { const panel = new Element(); const enable = new Element(); const next = new Element(); + const prev = new Element(); const calls = []; + const cycleOptions = []; const subscribers = []; + const uiState = { audioState: 'stopped' }; const radio = { subscribe(callback) { subscribers.push(callback); @@ -51,10 +54,12 @@ function fixture() { endTuning() { calls.push('endTuning'); }, - cycleStation(direction) { + cycleStation(direction, options) { calls.push(['cycle', direction]); + cycleOptions.push(options); return true; }, + getUIState: () => uiState, }; const actions = { isRegistered: () => true, @@ -73,6 +78,7 @@ function fixture() { _radioPanel: panel, _radioEnableBtn: enable, _radioNextBtn: next, + _radioPrevBtn: prev, }, radio, actions, @@ -82,8 +88,11 @@ function fixture() { controls, enable, next, + prev, actions, calls, + cycleOptions, + uiState, subscribers, document, cleanup() { @@ -172,3 +181,31 @@ test('destruction releases an active tuner pointer after revoking listeners', () f.cleanup(); } }); + +test('Next and Previous select without playing until playback has started', () => { + const f = fixture(); + try { + f.controls.connect(); + for (const audioState of ['stopped', 'paused', 'error']) { + f.uiState.audioState = audioState; + f.next.dispatchEvent(new Event('click')); + f.prev.dispatchEvent(new Event('click')); + } + assert.deepEqual( + f.cycleOptions.map((options) => options.autoplay), + [false, false, false, false, false, false], + ); + f.cycleOptions.length = 0; + for (const audioState of ['loading', 'buffering', 'playing']) { + f.uiState.audioState = audioState; + f.next.dispatchEvent(new Event('click')); + f.prev.dispatchEvent(new Event('click')); + } + assert.deepEqual( + f.cycleOptions.map((options) => options.autoplay), + [true, true, true, true, true, true], + ); + } finally { + f.cleanup(); + } +});