mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Keep Radio playback state consistent after a silent Next
Review follow-ups for HAL-722. A Next or Previous that only selects a station now leaves nothing of the previous stream behind: - Selecting another station without playing stops a paused or failed stream, so the panel, Stop, voice summaries and the audio element agree on "stopped". A fallback the caller armed for the new selection is kept. - A silent UI step arms no broadcaster fallback, and a user Play of the selected station drops one left by a cancelled voice step. Voice steps pass `fallback: true` because voice starts the prepared playback later. - A user step to another station emits a `select` playback control, and the realtime voice handoff treats it like Pause and Stop. - A tuner release onto a station the directory no longer has stops the stream the drag paused, publishing one state with the gesture's origin, so the button reads PLAY and a voice handoff is cancelled. - The transport buttons offer Pause while a stream connects, matching what a click does. - The privacy note names every action that connects to a broadcaster (play, finishing tuning, choosing a station on the globe) in all locales. The Next/Previous layer tests live in src/data/radioNextState.test.mjs.
This commit is contained in:
1 parent
b775bfed70
commit
4dd5a313b3
20 files changed
+523
-71
No files matched your search
@@ -2211,7 +2211,11 @@ export async function controlRadio(
|
||||
if (!radioActionIsCurrent(options)) return cancelled(summarize);
|
||||
const prepared = state.selected
|
||||
? true
|
||||
: radio.cycleStation?.(1, { focus: false, autoplay: false });
|
||||
: radio.cycleStation?.(1, {
|
||||
focus: false,
|
||||
autoplay: false,
|
||||
fallback: true,
|
||||
});
|
||||
return {
|
||||
ok: Boolean(prepared),
|
||||
action: 'control_radio',
|
||||
@@ -2235,6 +2239,7 @@ export async function controlRadio(
|
||||
const selected = radio.cycleStation?.(action === 'next' ? 1 : -1, {
|
||||
focus: false,
|
||||
autoplay: false,
|
||||
fallback: true,
|
||||
});
|
||||
return {
|
||||
ok: Boolean(selected),
|
||||
|
||||
@@ -1850,7 +1850,7 @@ test('voice Radio resolves Austin and exposes semantic selection, volume, pause,
|
||||
assert.equal(result.radioAction, 'play');
|
||||
assert.equal(result.stationId, 'nearest');
|
||||
assert.equal('station' in result, false);
|
||||
assert.deepEqual(calls.at(-1), ['cycle', 1, { focus: false, autoplay: false }]);
|
||||
assert.deepEqual(calls.at(-1), ['cycle', 1, { focus: false, autoplay: false, fallback: true }]);
|
||||
|
||||
for (const invalidCoordinates of [
|
||||
{ latitude: null, longitude: null },
|
||||
|
||||
@@ -593,46 +593,55 @@ test('replacement input invalidates an in-flight Radio preflight immediately', (
|
||||
assert.equal(controller.radioHandoffEpoch, priorEpoch + 1);
|
||||
});
|
||||
|
||||
test('direct Radio pause or stop cancels a pending voice handoff', async () => {
|
||||
const order = [];
|
||||
let playbackControl = null;
|
||||
const ui = {
|
||||
root: { dataset: {}, classList: { remove() {} }, querySelectorAll: () => [] },
|
||||
status: { textContent: '' },
|
||||
detail: { textContent: '', title: '' },
|
||||
errorDetail: { textContent: '' },
|
||||
};
|
||||
const controller = new GevRealtimeController({
|
||||
ui,
|
||||
runner: async () => ({ ok: true }),
|
||||
radioLayer: {
|
||||
subscribePlaybackControls(listener) {
|
||||
playbackControl = listener;
|
||||
return () => { playbackControl = null; };
|
||||
for (const [name, control, message] of [
|
||||
['direct Radio pause or stop cancels a pending voice handoff', 'stop', 'the explicit stop remains authoritative'],
|
||||
[
|
||||
'a direct Radio Next that does not play cancels a pending voice handoff',
|
||||
{ action: 'select', origin: 'user', attemptId: null },
|
||||
'the user selection is not overridden by voice playback',
|
||||
],
|
||||
]) {
|
||||
test(name, async () => {
|
||||
const order = [];
|
||||
let playbackControl = null;
|
||||
const ui = {
|
||||
root: { dataset: {}, classList: { remove() {} }, querySelectorAll: () => [] },
|
||||
status: { textContent: '' },
|
||||
detail: { textContent: '', title: '' },
|
||||
errorDetail: { textContent: '' },
|
||||
};
|
||||
const controller = new GevRealtimeController({
|
||||
ui,
|
||||
runner: async () => ({ ok: true }),
|
||||
radioLayer: {
|
||||
subscribePlaybackControls(listener) {
|
||||
playbackControl = listener;
|
||||
return () => { playbackControl = null; };
|
||||
},
|
||||
playForVoice: async () => {
|
||||
order.push('radio-play');
|
||||
return true;
|
||||
},
|
||||
stopPlayback: () => order.push('radio-stop'),
|
||||
},
|
||||
playForVoice: async () => {
|
||||
order.push('radio-play');
|
||||
return true;
|
||||
},
|
||||
stopPlayback: () => order.push('radio-stop'),
|
||||
},
|
||||
});
|
||||
controller.debugLog = () => {};
|
||||
controller.dc = {
|
||||
readyState: 'open',
|
||||
send() {},
|
||||
close() { order.push('voice-stop'); },
|
||||
};
|
||||
controller.pendingRadioPlaybackResult = { ok: true, radioPlaybackRequested: true };
|
||||
});
|
||||
controller.debugLog = () => {};
|
||||
controller.dc = {
|
||||
readyState: 'open',
|
||||
send() {},
|
||||
close() { order.push('voice-stop'); },
|
||||
};
|
||||
controller.pendingRadioPlaybackResult = { ok: true, radioPlaybackRequested: true };
|
||||
|
||||
playbackControl('stop');
|
||||
await controller.handleRealtimeEvent({
|
||||
data: JSON.stringify({ type: 'response.done', response: { status: 'completed' } }),
|
||||
});
|
||||
playbackControl(control);
|
||||
await controller.handleRealtimeEvent({
|
||||
data: JSON.stringify({ type: 'response.done', response: { status: 'completed' } }),
|
||||
});
|
||||
|
||||
assert.equal(controller.pendingRadioPlaybackResult, null);
|
||||
assert.deepEqual(order, [], 'the explicit stop remains authoritative');
|
||||
});
|
||||
assert.equal(controller.pendingRadioPlaybackResult, null);
|
||||
assert.deepEqual(order, [], message);
|
||||
});
|
||||
}
|
||||
|
||||
test('confirmed manual Radio playback closes active voice without stopping Radio', () => {
|
||||
const order = [];
|
||||
|
||||
@@ -65,7 +65,7 @@ export class RealtimeRadio {
|
||||
: control || {};
|
||||
if (
|
||||
event.origin === 'user' &&
|
||||
(event.action === 'pause' || event.action === 'stop')
|
||||
['pause', 'stop', 'select'].includes(event.action)
|
||||
) {
|
||||
this.cancelRadioHandoff();
|
||||
} else if (
|
||||
|
||||
Reference in new issue
Block a user