1import { describe, expect, it } from 'vitest'; 2import { readdirSync, readFileSync } from 'node:fs'; 3import { fetchMoves, fetchTakes, formMap, positions, writtenOrder } from './takes.mjs'; 4 5// every site song (not allSongs.test.mjs's songDirs: importing a test file 6// would run its tests here too) 7const SONGS = new URL('../../../songs/', import.meta.url).pathname; 8const siteSongs = () => 9 readdirSync(SONGS, { withFileTypes: true }) 10 .filter((t) => t.isDirectory()) 11 .flatMap((t) => 12 readdirSync(`${SONGS}${t.name}`, { withFileTypes: true }) 13 .filter((d) => d.isDirectory()) 14 .map((d) => ({ id: `${t.name}/${d.name}`, file: `${SONGS}${t.name}/${d.name}/song.js` })), 15 ); 16 17const moves = (sections) => ({ song: 'jev/x', performances: 2, of: 2, sections }); 18const s = (section, next = [], more = {}) => ({ section, plays: 1, asked: 1, fallbacks: 0, ms: null, next, ...more }); 19 20describe('writtenOrder', () => { 21 it("reads a walk() form's sections in the order they start", () => { 22 const code = `const form = walk(section, { 23 max: 16, 24 sections: { 25 drop: { at: 8, next: ['outro'] }, 26 intro: { at: 0, next: ['drop'] }, 27 outro:{at:12,next:[]}, 28 }, 29 })`; 30 expect(writtenOrder(code)).toEqual(['intro', 'drop', 'outro']); 31 }); 32 33 it('resolves `at: at.name` through the object it names, as LIGHTNING IN A BOTTLE writes it', () => { 34 const code = `const at = { intro: 0, riff: 8, outro: 16 } 35 walk(section, { sections: { riff: { at: at.riff, next: [] }, intro: { at: at.intro, next: [] }, outro: { at: at.outro, next: [] } } })`; 36 expect(writtenOrder(code)).toEqual(['intro', 'riff', 'outro']); 37 }); 38 39 it('claims no order it cannot read: an unresolved `at`, or sections built in code', () => { 40 expect(writtenOrder('walk(s, { sections: { a: { at: somewhere(), next: [] }, b: { at: 4, next: [] } } })')).toEqual([]); 41 expect(writtenOrder('const sections = { intro: { at: 0, next: [] } }\nfor (const s of SLOTS) sections[s] = { at: E }')).toEqual([]); 42 }); 43 44 it("reads every site song's form that is written out, and none that is built in code", () => { 45 const songs = siteSongs(); 46 expect(songs.length).toBeGreaterThan(10); 47 for (const { id, file } of songs) { 48 const order = writtenOrder(readFileSync(file, 'utf8')); 49 if (id === 'jev/all-green') expect(order, id).toEqual([]); 50 else expect(order.length, id).toBeGreaterThan(3); 51 } 52 }); 53 54 it('is empty for a song without a form', () => { 55 expect(writtenOrder('s("bd*4")')).toEqual([]); 56 expect(writtenOrder(undefined)).toEqual([]); 57 }); 58}); 59 60describe('formMap', () => { 61 it('lays the sections out as written, marking the written way on', () => { 62 const rows = formMap( 63 moves([ 64 s('drop', [{ section: 'outro', n: 2, share: 1 }]), 65 s('intro', [ 66 { section: 'drop', n: 3, share: 0.75 }, 67 { section: 'outro', n: 1, share: 0.25 }, 68 ]), 69 s('outro'), 70 s('gone'), 71 ]), 72 ['intro', 'drop', 'outro'], 73 ); 74 expect(rows.map((r) => r.section)).toEqual(['intro', 'drop', 'outro', 'gone']); 75 expect(rows[0].next.map((n) => [n.section, n.asWritten])).toEqual([ 76 ['drop', true], 77 ['outro', false], 78 ]); 79 expect(rows[2].writtenNext).toBeNull(); 80 expect(rows[3].writtenNext, 'a section the code no longer has').toBeNull(); 81 }); 82 83 it("orders a form it cannot read by the sections' places in the takes, claiming no written way on", () => { 84 const placed = positions([{ path: ['intro', 'guess', 'end'] }, { path: ['intro', 'guess', 'guess', 'end'] }, { path: null }]); 85 const rows = formMap(moves([s('end'), s('guess', [{ section: 'end', n: 2, share: 1 }]), s('intro')]), [], placed); 86 expect(rows.map((r) => [r.section, r.writtenNext])).toEqual([ 87 ['intro', null], 88 ['guess', null], 89 ['end', null], 90 ]); 91 expect(rows[1].next[0].asWritten).toBe(false); 92 }); 93 94 it('numbers the sections of a song without a form', () => { 95 const rows = formMap(moves([s('10'), s('2'), s('1')]), []); 96 expect(rows.map((r) => [r.section, r.writtenNext])).toEqual([ 97 ['1', '2'], 98 ['2', '3'], 99 ['10', '11'], 100 ]); 101 }); 102}); 103 104describe('fetching', () => { 105 it("asks for a song's takes a page at a time, and its moves", async () => { 106 const asked = []; 107 const fetchImpl = async (url) => (asked.push(url), new Response('{"ok":1}')); 108 expect(await fetchTakes('jev/dial-up', 20, fetchImpl)).toEqual({ ok: 1 }); 109 await fetchMoves('jev/dial-up', fetchImpl); 110 expect(asked).toEqual([ 111 '/jev/performances?song=jev%2Fdial-up&offset=20&limit=20', 112 '/jev/performances/moves?song=jev%2Fdial-up', 113 ]); 114 }); 115 116 it('throws on an error, so the view shows nothing rather than stale data', async () => { 117 await expect(fetchMoves('jev/x', async () => new Response('', { status: 503 }))).rejects.toThrow(/503/); 118 }); 119});