1import { describe, expect, it } from 'vitest'; 2import { cycleNow, groupTabs, progressOf, statusFor, worthSending } from './lobby.mjs'; 3 4const song = { id: 'jev/a', title: 'A', code: 'song a', timing: { bars: 62 } }; 5const walked = { id: 'jev/w', title: 'W', code: 'song w', timing: { jev: true } }; 6const base = { songs: [song, walked], nowId: 'jev/a', code: 'song a', foreign: null, visible: true, booth: null }; 7 8describe('statusFor: what this tab tells the lobby', () => { 9 it('a site song as written, playing: its id, playhead and length', () => { 10 expect(statusFor({ ...base, started: true, cycle: 12.34, cps: 0.5 })).toEqual({ 11 t: 'status', 12 visible: true, 13 song: 'jev/a', 14 edited: false, 15 listener: null, 16 title: null, 17 playing: true, 18 cycle: 12.3, 19 cps: 0.5, 20 length: 64, 21 max: null, 22 section: null, 23 }); 24 }); 25 26 it('an edited song says so, and has no length', () => { 27 expect(statusFor({ ...base, code: 'mine now', started: false, cycle: null, cps: 0.5 })).toMatchObject({ 28 song: 'jev/a', 29 edited: true, 30 playing: false, 31 cycle: null, 32 cps: null, 33 length: null, 34 }); 35 }); 36 37 it('a song Jev walks: its section by name, and the most it can last until Jev ends it', () => { 38 const booth = { 39 every: 8, 40 segments: 16, 41 last: null, 42 form: { name: 'section' }, 43 decisions: [{ values: { section: { choice: 'intro' } } }, { values: { section: { choice: 'drop1' } } }], 44 }; 45 const s = statusFor({ ...base, nowId: 'jev/w', code: 'song w', started: true, cycle: 9, cps: 0.5, booth }); 46 expect(s).toMatchObject({ song: 'jev/w', length: null, max: 128, section: 'drop1' }); 47 const ended = statusFor({ ...base, nowId: 'jev/w', code: 'song w', started: true, cycle: 9, cps: 0.5, booth: { ...booth, last: 5 } }); 48 expect(ended.length).toBe(50); 49 }); 50 51 it('a listener’s song in the sandbox: its id and title, never a site song', () => { 52 const foreign = { song: { id: 'listener:abc123', title: 'Theirs' } }; 53 expect(statusFor({ ...base, foreign, started: true, cycle: 3, cps: 1 })).toMatchObject({ 54 song: null, 55 listener: 'abc123', 56 title: 'Theirs', 57 playing: true, 58 }); 59 }); 60 61 it('the listener’s own code, or code from an AI or a link: nothing but whether it plays', () => { 62 expect(statusFor({ ...base, nowId: null, code: 'x', foreign: { ai: 'Claude' }, started: true, cycle: 1, cps: 1 })).toMatchObject({ 63 song: null, 64 listener: null, 65 title: null, 66 playing: true, 67 }); 68 }); 69}); 70 71describe('worthSending', () => { 72 const s = statusFor({ ...base, started: true, cycle: 10, cps: 0.5 }); 73 it('sends a change, not the playhead moving as predicted', () => { 74 expect(worthSending(null, s, 0)).toBe(true); 75 expect(worthSending(s, { ...s, cycle: 11 }, 2000)).toBe(false); 76 expect(worthSending(s, { ...s, cycle: 14 }, 2000)).toBe(true); 77 expect(worthSending(s, { ...s, section: 'drop1' }, 1000)).toBe(true); 78 expect(worthSending(s, { ...s, visible: false }, 1000)).toBe(true); 79 expect(worthSending(s, { ...s, cycle: 25 }, 30_000)).toBe(true); 80 }); 81}); 82 83describe('reading other tabs', () => { 84 const tab = { id: 't1', group: 'g', name: null, playing: true, cycle: 10, cps: 0.5, at: 1000, length: 40 }; 85 it('moves a playhead on at its tempo, in the room’s clock', () => { 86 expect(cycleNow(tab, 5000)).toBe(12); 87 expect(progressOf(tab, 5000)).toBeCloseTo(0.3); 88 expect(progressOf({ ...tab, length: null }, 5000)).toBeNull(); 89 expect(cycleNow({ ...tab, playing: false, cycle: null }, 5000)).toBeNull(); 90 expect(progressOf(tab, 1e9)).toBe(1); 91 }); 92 93 it('groups an account’s tabs, most recently heard first', () => { 94 const groups = groupTabs([ 95 { id: 'a', group: 'g1', name: 'Chris', at: 1 }, 96 { id: 'b', group: 'x', name: null, at: 3 }, 97 { id: 'c', group: 'g1', name: 'Chris', at: 2 }, 98 ]); 99 expect(groups.map((g) => [g.name, g.tabs.map((t) => t.id)])).toEqual([ 100 [null, ['b']], 101 ['Chris', ['c', 'a']], 102 ]); 103 }); 104});