mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 03:58:32 +00:00
feat(career): tuning preference filter + interstitial for gigs
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
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a57b62378c
commit
2a455702b8
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user