From 28b0319e2738fae77e9450d8aefd3c02ee0b0b1f Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Thu, 2 Jul 2026 13:50:56 -0500 Subject: [PATCH] =?UTF-8?q?play-queue:=20peekNext()=20=E2=80=94=20expose?= =?UTF-8?q?=20the=20following=20track=20for=20queue-aware=20UIs=20(#719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A results screen that offers "Up next: — starting in 10s" needs to know WHAT follows without reaching into queue internals. peekNext() returns {filename, index, total} for the next track (null when nothing follows), pure — peeking never plays or mutates. First consumer: the note_detect results card's queue-advance strip (the "Playlist Play All has no way to progress" tester issue). Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN Co-authored-by: Claude Fable 5 --- static/app.js | 8 +++++ tests/js/play_queue_peek.test.js | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/js/play_queue_peek.test.js diff --git a/static/app.js b/static/app.js index 256a465..c9c5ed7 100644 --- a/static/app.js +++ b/static/app.js @@ -6778,6 +6778,14 @@ window.feedBack.playQueue = (function () { start: start, advance: advance, hasNext: hasNext, active: active, clear: clear, source: function () { return source; }, remaining: function () { return active() ? list.length - idx - 1 : 0; }, + // What's coming, for consumers that RENDER the queue (a results + // screen's "Up next: … starting in 10s" strip) without reaching into + // queue internals. Null when nothing follows. + peekNext: function () { + return hasNext() + ? { filename: list[idx + 1], index: idx + 1, total: list.length } + : null; + }, }; })(); diff --git a/tests/js/play_queue_peek.test.js b/tests/js/play_queue_peek.test.js new file mode 100644 index 0000000..8f0cbbc --- /dev/null +++ b/tests/js/play_queue_peek.test.js @@ -0,0 +1,51 @@ +// playQueue.peekNext() (queue-advance UX): consumers that render "Up next" +// (the results card's countdown strip) need to know WHAT follows without +// reaching into queue internals. Extract the playQueue IIFE from app.js and +// drive it against a playSong stub. +'use strict'; +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +function makeQueue() { + const src = fs.readFileSync(path.join(__dirname, '..', '..', 'static', 'app.js'), 'utf8'); + const start = src.indexOf('window.feedBack.playQueue = (function () {'); + assert.ok(start !== -1, 'playQueue IIFE found in app.js'); + const end = src.indexOf('})();', start); + assert.ok(end !== -1, 'playQueue IIFE terminator found'); + const iife = src.slice(start, end + 5); + const played = []; + const sandbox = { + window: { + feedBack: {}, + playSong: (fn, arr, opts) => played.push({ fn, arr, opts }), + fbNotify: null, + }, + encodeURIComponent, + }; + // eslint-disable-next-line no-new-func + new Function('window', 'encodeURIComponent', iife)(sandbox.window, encodeURIComponent); + return { q: sandbox.window.feedBack.playQueue, played }; +} + +test('peekNext exposes the following track without mutating the queue', () => { + const { q, played } = makeQueue(); + assert.strictEqual(q.peekNext(), null); // idle queue → null + q.start(['a.sloppak', 'b.sloppak', 'c.sloppak'], { source: 'My list' }); + assert.deepStrictEqual(q.peekNext(), { filename: 'b.sloppak', index: 1, total: 3 }); + assert.deepStrictEqual(q.peekNext(), { filename: 'b.sloppak', index: 1, total: 3 }); // pure + assert.strictEqual(played.length, 1); // peeking never plays + q.advance(); + assert.deepStrictEqual(q.peekNext(), { filename: 'c.sloppak', index: 2, total: 3 }); + q.advance(); + assert.strictEqual(q.peekNext(), null); // last track → nothing next + assert.strictEqual(q.remaining(), 0); +}); + +test('peekNext is null after clear', () => { + const { q } = makeQueue(); + q.start(['a.sloppak', 'b.sloppak']); + q.clear(); + assert.strictEqual(q.peekNext(), null); +});