mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 01:04:29 +00:00
Users can now pick a tuning preference before booking a gig: - Any (default), Standard only, Drop only, or a specific tuning - Backend filters the song pool (stubs + filler) by that preference - Empty-filter case returns a 404 with a descriptive message; frontend reverts the pref to 'any' and shows a notification - Interstitial pause before first song and on tuning changes (all prefs except 'specific') via window.feedBack.holdAutoplay(); opens the tuner panel in auto mode while the user retunes - 'Specific' gigs skip interstitials (every song already shares one tuning) - Graceful degradation: no holdAutoplay → interstitial silently skipped New backend: - _tuning_ok_fn helper for standard/drop/specific classification - _fill_genre_songs accepts optional tuning_ok filter - propose_gig batch-fetches tuning_name for played stubs, applies filter - GET /gigs/tunings endpoint for the specific-tuning picker Tests: - tests/test_career_gig_tuning.py — 17 Python tests (classification, filter) - tests/js/career_gig_tuning.test.js — 9 JS tests (interstitial logic) - tests/plugins/career/conftest.py — songs table schema gets tuning_name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
169 lines
6.3 KiB
JavaScript
169 lines
6.3 KiB
JavaScript
/**
|
|
* Tests for career-gig-tuning interstitial logic (feedBack career-gig-tuning).
|
|
*
|
|
* Failure inputs:
|
|
* - pref='specific' + first song → no interstitial (specific never needs a tune pause)
|
|
* - pref='any' + first song → interstitial fires
|
|
* - pref='any' + same tuning → no interstitial between songs
|
|
* - pref='any' + tuning diff → interstitial fires
|
|
* - holdAutoplay absent → interstitial gracefully skipped
|
|
*/
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
const ROOT = path.join(__dirname, '..', '..');
|
|
const SCREEN_JS = fs.readFileSync(
|
|
path.join(ROOT, 'plugins', 'career', 'screen.js'), 'utf8'
|
|
);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// VM harness
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeCtx(opts = {}) {
|
|
const holdReleaseCalled = { v: false };
|
|
const holdSettleCalled = { v: false };
|
|
|
|
const feedBackBase = {
|
|
on: () => {},
|
|
emit: () => {},
|
|
holdAutoplay: opts.noHoldAutoplay ? undefined : function () {
|
|
const release = function () { holdReleaseCalled.v = true; };
|
|
release.settle = function () { holdSettleCalled.v = true; };
|
|
return release;
|
|
},
|
|
};
|
|
|
|
const ctx = vm.createContext({
|
|
window: {},
|
|
document: {
|
|
getElementById: () => null,
|
|
readyState: 'complete',
|
|
addEventListener: () => {},
|
|
},
|
|
localStorage: {
|
|
_store: {},
|
|
getItem(k) { return this._store[k] != null ? this._store[k] : null; },
|
|
setItem(k, v) { this._store[k] = String(v); },
|
|
},
|
|
clearTimeout: () => {},
|
|
setTimeout: (fn, ms) => 42,
|
|
fetch: () => Promise.resolve({ ok: false, json: async () => ({}) }),
|
|
console,
|
|
__holdReleaseCalled: holdReleaseCalled,
|
|
__holdSettleCalled: holdSettleCalled,
|
|
});
|
|
|
|
// Set window.feedBack inside the context so script-level refs pick it up
|
|
ctx.window.feedBack = feedBackBase;
|
|
|
|
vm.runInContext(SCREEN_JS, ctx);
|
|
return ctx;
|
|
}
|
|
|
|
function setRun(ctx, tuning_pref, songs) {
|
|
vm.runInContext(`
|
|
window.__careerPassportTest.setGigRun({
|
|
idx: 0,
|
|
tuning_pref: ${JSON.stringify(tuning_pref)},
|
|
songs: ${JSON.stringify(songs)},
|
|
});
|
|
`, ctx);
|
|
}
|
|
|
|
function get(ctx, expr) {
|
|
return vm.runInContext(expr, ctx);
|
|
}
|
|
|
|
function callOnLoading(ctx) {
|
|
vm.runInContext('window.__careerPassportTest.onGigSongLoading()', ctx);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('career-gig-tuning interstitial', () => {
|
|
test('no hold when gig run is null', () => {
|
|
const ctx = makeCtx();
|
|
vm.runInContext('window.__careerPassportTest.setGigRun(null)', ctx);
|
|
callOnLoading(ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('first song fires interstitial for pref=any', () => {
|
|
const ctx = makeCtx();
|
|
setRun(ctx, 'any', [{ filename: 'a.sloppak', tuning_name: 'E Standard' }]);
|
|
callOnLoading(ctx);
|
|
assert.notEqual(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('first song fires interstitial for pref=standard', () => {
|
|
const ctx = makeCtx();
|
|
setRun(ctx, 'standard', [{ filename: 'a.sloppak', tuning_name: 'E Standard' }]);
|
|
callOnLoading(ctx);
|
|
assert.notEqual(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('first song does NOT fire interstitial for pref=specific', () => {
|
|
const ctx = makeCtx();
|
|
setRun(ctx, 'specific', [{ filename: 'a.sloppak', tuning_name: 'E Standard' }]);
|
|
callOnLoading(ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('between-songs same tuning: no interstitial', () => {
|
|
const ctx = makeCtx();
|
|
const songs = [
|
|
{ filename: 'a.sloppak', tuning_name: 'E Standard' },
|
|
{ filename: 'b.sloppak', tuning_name: 'E Standard' },
|
|
];
|
|
setRun(ctx, 'any', songs);
|
|
vm.runInContext('window.__careerPassportTest.setLastTuning("E Standard")', ctx);
|
|
vm.runInContext('window.__careerPassportTest.getGigRun().idx = 1', ctx);
|
|
callOnLoading(ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('between-songs tuning change: fires interstitial for pref=any', () => {
|
|
const ctx = makeCtx();
|
|
const songs = [
|
|
{ filename: 'a.sloppak', tuning_name: 'E Standard' },
|
|
{ filename: 'b.sloppak', tuning_name: 'Drop D' },
|
|
];
|
|
setRun(ctx, 'any', songs);
|
|
vm.runInContext('window.__careerPassportTest.setLastTuning("E Standard")', ctx);
|
|
vm.runInContext('window.__careerPassportTest.getGigRun().idx = 1', ctx);
|
|
callOnLoading(ctx);
|
|
assert.notEqual(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('lastTuning is updated after onGigSongLoading', () => {
|
|
const ctx = makeCtx();
|
|
setRun(ctx, 'any', [{ filename: 'a.sloppak', tuning_name: 'Drop D' }]);
|
|
callOnLoading(ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getLastTuning()'), 'Drop D');
|
|
});
|
|
|
|
test('clearing hold via setTuningHold(null) leaves null', () => {
|
|
const ctx = makeCtx();
|
|
setRun(ctx, 'any', [{ filename: 'a.sloppak', tuning_name: 'E Standard' }]);
|
|
callOnLoading(ctx);
|
|
assert.notEqual(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
vm.runInContext('window.__careerPassportTest.setTuningHold(null)', ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
|
|
test('holdAutoplay unavailable: no interstitial (graceful skip)', () => {
|
|
const ctx = makeCtx({ noHoldAutoplay: true });
|
|
setRun(ctx, 'any', [{ filename: 'a.sloppak', tuning_name: 'E Standard' }]);
|
|
callOnLoading(ctx);
|
|
assert.equal(get(ctx, 'window.__careerPassportTest.getTuningHold()'), null);
|
|
});
|
|
});
|