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); +});