mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 12:21:49 +00:00
Some checks are pending
ship-ci / ci (push) Waiting to run
Tester, mid-gig: "the second song in the gig started when the first one ended. But it showed the flyover intro again." The flyover is arriving at the venue, and you arrive once. #968 stopped it replaying on an arrangement SWITCH (same filename), but a gig's song 2 is a genuinely different file, so it took the full-teardown path and played the arrival flyover again — the camera flew in from the back of the room before every track of the set. The play queue now answers isContinuation(): false for the first song of a set (or a standalone play — an arrival), true for song 2..N. onSongLoaded carries the room over to the new song's loop on a continuation, and only a real arrival plays the intro. Verified on the built AppImage: isContinuation goes false (song 1) -> true (song 2) across an advance, and song 2 no longer flies in. Also confirmed NOT a bug, same session: "didn't show the author for the second song." The credits card shows on a queue advance whenever the song carries authors — reproduced with a song that has them as the advanced-to track. The tester's song 2 simply had no `authors:` metadata (most auto-converted feedpaks don't). No code change. Tests: isContinuation across start/advance/clear, and that onSongLoaded gates the flyover on the continuation check. Both fail on pre-fix source. JS 1214/1214.
129 lines
6.3 KiB
JavaScript
129 lines
6.3 KiB
JavaScript
// 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);
|
|
});
|
|
|
|
// A gig/album/playlist queue must survive a playSong wrapper that drops the
|
|
// options object.
|
|
//
|
|
// The queue tells playSong "don't clear the queue I'm driving" via
|
|
// options.fromQueue. But a chain of plugin playSong wrappers (nam_tone,
|
|
// midi_amp, fretboard, invert_highway, tabview, ...) forward only
|
|
// (filename, arrangement) and silently drop the 3rd arg. With just the in-band
|
|
// flag, playSong cleared the queue the instant its first song started, so a gig
|
|
// never advanced (feedBack#… tester: "Passports does not advance in the song
|
|
// queue"). The queue now also raises an out-of-band flag, _consumeInternalPlay(),
|
|
// which playSong honours regardless of the wrapper chain.
|
|
|
|
// The real clear-guard from session.js, driven against the queue.
|
|
function clearGuard(win, options) {
|
|
const pq = win.feedBack && win.feedBack.playQueue;
|
|
const queueDriven = (options && options.fromQueue)
|
|
|| (pq && typeof pq._consumeInternalPlay === 'function' && pq._consumeInternalPlay());
|
|
if (!queueDriven && pq) pq.clear();
|
|
}
|
|
|
|
test('the queue survives a playSong that drops the options arg', () => {
|
|
const { q } = makeQueue();
|
|
// Rebind the queue's window.playSong to a wrapper that forwards ONLY
|
|
// (filename, arrangement) — exactly the plugin bug — and runs the real guard.
|
|
const win = { feedBack: { playQueue: q } };
|
|
// Reach the same window the IIFE closed over: re-drive through the guard by
|
|
// calling start and simulating what _play's playSong does.
|
|
// We can't rebind the closed-over window, so instead assert the out-of-band
|
|
// signal directly: _play sets it, and the guard consumes it.
|
|
q.start(['a.sloppak', 'b.sloppak', 'c.sloppak'], { source: 'gig' });
|
|
// After start()->_play, the internal flag was set; the guard (which the real
|
|
// playSong runs) must see it as queue-driven and NOT clear.
|
|
win.feedBack.playQueue = q;
|
|
clearGuard(win, undefined /* wrapper dropped options */);
|
|
assert.strictEqual(q.active(), true, 'a dropped options arg must not clear the queue');
|
|
assert.strictEqual(q.remaining(), 2, 'the queue must still have its remaining tracks');
|
|
});
|
|
|
|
test('_consumeInternalPlay is one-shot — a later MANUAL play still clears', () => {
|
|
const { q } = makeQueue();
|
|
q.start(['a.sloppak', 'b.sloppak'], { source: 'album' });
|
|
const win = { feedBack: { playQueue: q } };
|
|
// First guard call (the queue's own play) consumes the flag → no clear.
|
|
clearGuard(win, undefined);
|
|
assert.strictEqual(q.active(), true);
|
|
// A subsequent MANUAL play (no fromQueue, flag already consumed) must clear.
|
|
clearGuard(win, undefined);
|
|
assert.strictEqual(q.active(), false, 'a manual play after the queue play must abandon the queue');
|
|
});
|
|
|
|
test('fromQueue in options still works on its own (in-band path)', () => {
|
|
const { q } = makeQueue();
|
|
q.start(['a.sloppak', 'b.sloppak'], { source: 'gig' });
|
|
// consume the internal flag first so ONLY options.fromQueue is under test
|
|
q._consumeInternalPlay();
|
|
const win = { feedBack: { playQueue: q } };
|
|
clearGuard(win, { fromQueue: true });
|
|
assert.strictEqual(q.active(), true, 'options.fromQueue alone must still keep the queue');
|
|
});
|
|
|
|
// isContinuation(): true for song 2..N of a set, false for the first song / a
|
|
// standalone play. The venue uses it to fly in once on arrival, then carry the
|
|
// room between songs instead of replaying the arrival flyover every track
|
|
// (tester: "it showed the flyover intro again" on a gig's second song).
|
|
test('isContinuation is false on the first song, true after advancing', () => {
|
|
const { q } = makeQueue();
|
|
assert.strictEqual(q.isContinuation(), false, 'idle queue is not a continuation');
|
|
q.start(['a.sloppak', 'b.sloppak', 'c.sloppak'], { source: 'gig' });
|
|
assert.strictEqual(q.isContinuation(), false, 'the FIRST song of a set is an arrival, not a continuation');
|
|
q.advance();
|
|
assert.strictEqual(q.isContinuation(), true, 'song 2 is a continuation — no re-flyover');
|
|
q.advance();
|
|
assert.strictEqual(q.isContinuation(), true, 'song 3 too');
|
|
q.clear();
|
|
assert.strictEqual(q.isContinuation(), false, 'a cleared queue is not a continuation');
|
|
});
|