1// The song view's takes and form map, rendered from the Worker's real 2// answers (worker/src/listening.ts against testD1): what they show, and a 3// section name as text, never markup. 4import { createElement } from 'react'; 5import { renderToString } from 'react-dom/server'; 6import { describe, expect, it } from 'vitest'; 7import { testD1 } from '../../../worker/test/d1'; 8import { listening } from '../../../worker/src/listening'; 9import { d1Listening } from '../../../worker/src/listening-store'; 10import { FormMap, Takes } from './JevTakes.jsx'; 11 12const hash = 'a'.repeat(64); 13const segment = (section, status = 'answered', ms = 90) => ({ status, ms, values: { section: { choice: section } } }); 14const performance = (sections, fallbackAt = -1) => ({ 15 song: 'jev/dial-up', 16 codeHash: hash, 17 model: 'jev-1.13.0', 18 every: 4, 19 ended: 'finished', 20 form: { jev: 0, question: 'section' }, 21 jevs: [{ segments: sections.map((s, i) => segment(s, i === fallbackAt ? 'fallback' : 'answered')) }], 22}); 23const env = { LISTEN_LIMIT: { limit: async () => ({ success: true }) }, RECORD_LIMIT: { limit: async () => ({ success: true }) } }; 24const get = async (store, path) => (await listening(new Request(`http://x${path}`), env, store)).json(); 25 26async function answers() { 27 const store = d1Listening(testD1()); 28 await store.record('a'.repeat(22), '2026-09-26', performance(['intro', 'verse', 'drop', 'outro'])); 29 await store.record('b'.repeat(22), '2026-09-27', performance(['intro', 'drop', 'drop', 'outro'], 2)); 30 await store.record('c'.repeat(22), '2026-09-27', performance(['intro', 'x_script_alert', 'outro'])); 31 return { 32 takes: await get(store, '/jev/performances?song=jev/dial-up'), 33 moves: await get(store, '/jev/performances/moves?song=jev/dial-up'), 34 }; 35} 36 37const song = { 38 id: 'jev/dial-up', 39 code: `walk(section, { sections: { intro: { at: 0, next: [] }, verse: { at: 4, next: [] }, drop: { at: 8, next: [] }, outro: { at: 12, next: [] } } })`, 40}; 41const text = (html) => html.replaceAll('<!-- -->', '').replace(/<[^>]*>/g, ' ').replace(/'/g, "'").replace(/\s+/g, ' '); 42 43describe("a song's takes", () => { 44 it('lists each take newest first, with its path, its fallbacks and a replay link', async () => { 45 const { takes } = await answers(); 46 const html = renderToString(createElement(Takes, { song, first: takes })); 47 expect(text(html)).toMatch(/Jev's takes · 3/); 48 expect(text(html)).toMatch(/intro → drop → drop → outro/); 49 expect(text(html)).toMatch(/1 unanswered/); 50 expect(html).toContain(`href="/songs/jev/dial-up/?performance=${'b'.repeat(22)}"`); 51 expect(html.indexOf('c'.repeat(22)) < html.indexOf('a'.repeat(22)), 'newest first').toBe(true); 52 }); 53}); 54 55describe('the form map', () => { 56 it('lays the sections out as written, with the ways Jev took and how often', async () => { 57 const { takes, moves } = await answers(); 58 const html = renderToString(createElement(FormMap, { song, moves, takes: takes.takes })); 59 const t = text(html); 60 expect(t).toMatch(/over 3 takes/); 61 expect(t.indexOf('intro played') < t.indexOf('verse played')).toBe(true); 62 expect(t.indexOf('verse played') < t.indexOf('drop played')).toBe(true); 63 // intro went to verse once (as written), drop once, and to the unknown section once 64 expect(t).toMatch(/intro played 3× · answered in 90 ms \(p90 90\) → (drop|verse|x_script_alert) 33%/); 65 expect(t).toMatch(/verse 33% \(as written\)/); 66 expect(t).toMatch(/drop played 3× · Jev unreachable 1 of 3/); 67 expect(html).toContain('<svg'); 68 }); 69 70 it('shows a section name as text', () => { 71 const name = 'a<b'; // not a section name the Worker accepts; rendered anyway as text 72 const moves = { song: 'jev/dial-up', performances: 1, of: 1, sections: [{ section: name, plays: 1, asked: 1, fallbacks: 0, ms: null, next: [] }] }; 73 const html = renderToString(createElement(FormMap, { song, moves, takes: [] })); 74 expect(html).not.toContain('a<b'); 75 expect(html).toContain('a<b'); 76 }); 77}); 78 79describe("a listener song's takes", () => { 80 it('replay in the sandbox, say who played them and when, and map the form from the code it was written as', async () => { 81 const db = testD1(); 82 const player = 'U'.repeat(22); 83 await db.prepare('INSERT INTO users (id, display_name, created_at) VALUES (?, ?, 1)').bind(player, 'Link').run(); 84 const store = d1Listening(db); 85 const id = `listener:${'L'.repeat(22)}`; 86 const at = Date.parse('2026-09-27T14:05:31Z'); 87 await store.record('a'.repeat(22), '2026-09-27', { ...performance(['intro', 'drop', 'outro']), song: id }, { at, player }); 88 await store.record('b'.repeat(22), '2026-09-27', { ...performance(['intro', 'outro']), song: id }, { at: at + 1, player: null }); 89 const takes = await get(store, `/jev/performances?song=${encodeURIComponent(id)}`); 90 const moves = await get(store, `/jev/performances/moves?song=${encodeURIComponent(id)}`); 91 // a listener song carries its code as foreignCode, never code 92 const listenerSong = { id, foreignCode: song.code }; 93 const html = renderToString(createElement(Takes, { song: listenerSong, first: takes })); 94 const t = text(html); 95 expect(html).toContain(`href="/?listener=${'L'.repeat(22)}&performance=${'a'.repeat(22)}"`); 96 expect(t).toMatch(/2026-09-27 14:05 UTC · Link · finished/); 97 expect(t).toMatch(/a listener · finished/); 98 const map = text(renderToString(createElement(FormMap, { song: listenerSong, moves, takes: takes.takes }))); 99 expect(map.indexOf('intro played') < map.indexOf('drop played')).toBe(true); 100 expect(map).toMatch(/\(as written\)/); 101 }); 102});