jevstrudel.git / website / src / jev / measured.test.mjs
1import { describe, expect, it } from 'vitest';
2import {
3  measuredFor,
4  readMeasured,
5  sectionCycles,
6  sectionSpans,
7  setMeasured,
8  songHash,
9  withoutSectionParts,
10} from './measured.mjs';
11import { artRequest } from './critic.mjs';
12
13const spec = '---\ntitle: X\nsong: song.js\ncritic:\n  art: 0.6\n---\n\n# X\n';
14const code = 'setcps(1)\n';
15const m = { date: '2026-09-24', song: songHash(code), total: { loudnessDbfs: -16 }, parts: { riff: { loudnessDbfs: -20 } } };
16
17describe('measured', () => {
18  it('writes one JSON line after song:, and reads it back', () => {
19    const written = setMeasured(spec, m);
20    expect(written).toMatch(/^song: song\.js\nmeasured: \{/m);
21    expect(readMeasured(written)).toEqual(m);
22    // replaces, never duplicates
23    const again = setMeasured(written, { ...m, date: '2026-09-25' });
24    expect(again.match(/^measured:/gm)).toHaveLength(1);
25    expect(readMeasured(again).date).toBe('2026-09-25');
26    expect(readMeasured(spec)).toBeNull();
27  });
28
29  it('holds only for the song.js it measured', () => {
30    expect(measuredFor(m, code)).toEqual(m);
31    expect(measuredFor(m, code + '// edited\n')).toBeNull();
32    expect(measuredFor(null, code)).toBeNull();
33  });
34
35  it('sections a song on its longest .every cadence, keyed by cycles from 1', () => {
36    expect(sectionCycles('a.every(4, x); b.every( 8, y)')).toBe(8);
37    expect(sectionCycles('no jev here')).toBe(1);
38    expect(sectionSpans(4, 10)).toEqual([
39      { key: '1-4', from: 0, to: 4 },
40      { key: '5-8', from: 4, to: 8 },
41      { key: '9-10', from: 8, to: 10 },
42    ]);
43  });
44
45  it('keeps sections on the one line, and gives the browser their totals only', () => {
46    const level = { loudnessDbfs: -18, peakDbfs: -3 };
47    const withSections = { ...m, sections: { '1-4': { total: level, parts: { riff: level } }, '5-8': null } };
48    const written = setMeasured(spec, withSections);
49    expect(written.split('\n').filter((l) => l.startsWith('measured:'))).toHaveLength(1);
50    expect(readMeasured(written)).toEqual(withSections);
51    expect(withoutSectionParts(withSections).sections).toEqual({ '1-4': { total: level }, '5-8': null });
52    expect(withoutSectionParts(m)).toEqual(m);
53    expect(withoutSectionParts(null)).toBeNull();
54    // the critic reads each section's total, whichever it is given
55    for (const given of [withSections, withoutSectionParts(withSections)]) {
56      const mix = artRequest({ title: 'X', spec: '', code, measured: given }).state.measuredMix;
57      expect(mix.sections).toEqual({ '1-4': level, '5-8': null });
58    }
59  });
60
61  it('gives the critic the mix, labelled, only when there is one', () => {
62    expect(artRequest({ title: 'X', spec: '', code }).state.measuredMix).toBeUndefined();
63    const mix = artRequest({ title: 'X', spec: '', code, measured: m }).state.measuredMix;
64    expect(mix.parts.riff.loudnessDbfs).toBe(-20);
65    expect(mix.what).toMatch(/dBFS/);
66  });
67});