Files
songtrace/tests/js/sheet.test.mjs
T
branderson 2aeb16decb webui: a grouped keyboard shortcut panel for the sheet
The sheet's controls were listed in one run-on line under it, which was
hard to scan and grew with every feature. Replace it with a panel grouped
the way an editor's reference card is (select, move, length, notes, kind,
syllable, line) with keys drawn as key caps, opened by a KEYS button or ?,
closed by Esc, and remembered open or closed across visits.

The list is data (SHORTCUTS), and a test checks every letter key it names
is one the sheet's key handler handles, so the two cannot drift apart.
2026-10-12 23:13:45 -07:00

73 lines
3.0 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { beatLines, isBlackKey, lineSpan, noteName, pitchRange }
from "../../src/songtrace/static/sheet.js";
test("note names follow UltraStar's C4 = 0", () => {
assert.equal(noteName(0), "C4");
assert.equal(noteName(9), "A4");
assert.equal(noteName(-1), "B3");
assert.equal(noteName(13), "C♯5");
assert.equal(noteName(-13), "B2");
});
test("black keys are the sharps, in any octave", () => {
assert.deepEqual([0, 1, 2, 3, 4, 5, 6].map(isBlackKey),
[false, true, false, true, false, false, true]);
assert.equal(isBlackKey(-11), true); // C#3
assert.equal(isBlackKey(-12), false); // C3
});
test("the pitch range pads the line's notes and keeps a minimum height", () => {
assert.deepEqual(pitchRange([0, 5]), { lo: -4, hi: 10 }); // 15 rows
assert.deepEqual(pitchRange([0, 20]), { lo: -3, hi: 23 }); // padded only
assert.deepEqual(pitchRange([]), { lo: -7, hi: 7 });
});
test("beat lines sit on the chart's grid with bar weights", () => {
// 300 BPM: an UltraStar beat is 60 / (4 * 300) = 0.05s, from GAP 1.0s
const lines = beatLines(1.0, 1.85, { bpm: 300, gap_ms: 1000 });
assert.equal(lines.length, 18);
assert.deepEqual(lines.slice(0, 5).map(b => b.weight), [2, 0, 0, 0, 1]);
assert.equal(lines[16].weight, 2);
assert.ok(Math.abs(lines[4].t - 1.2) < 1e-9);
});
test("beats before GAP keep their weights", () => {
const lines = beatLines(0.75, 1.0, { bpm: 300, gap_ms: 1000 });
assert.deepEqual(lines.map(b => [b.beat, b.weight]),
[[-5, 0], [-4, 1], [-3, 0], [-2, 0], [-1, 0], [0, 2]]);
});
test("a line spans its notes and words", () => {
assert.deepEqual(lineSpan({ notes: [{ start: 2, end: 3 }], words: [{ start: 1.5, end: 2.5 }] }),
{ start: 1.5, end: 3 });
assert.equal(lineSpan({ notes: [], words: [] }), null);
});
import { readFileSync } from "node:fs";
import { SHORTCUTS, comboParts } from "../../src/songtrace/static/sheet.js";
test("shortcut combos split into keys and mouse actions", () => {
assert.deepEqual(comboParts("Shift+click / Tab"), [
[{ text: "Shift", key: true }, { text: "click", key: false }],
[{ text: "Tab", key: true }],
]);
assert.deepEqual(comboParts("Page Up"), [[{ text: "Page Up", key: true }]]);
});
test("every letter key the shortcut list names is one the sheet handles", () => {
const source = readFileSync(new URL("../../src/songtrace/static/sheet.js", import.meta.url), "utf8");
const handler = source.slice(source.indexOf('plot.addEventListener("keydown"'));
const letters = SHORTCUTS.flatMap(([, entries]) => entries)
.flatMap(([combo]) => comboParts(combo).flat())
.filter(p => p.key && /^[A-Z]$/.test(p.text))
.map(p => p.text.toLowerCase());
assert.ok(letters.length >= 6);
for (const letter of new Set(letters)) {
assert.ok(handler.includes(`"${letter}"`) || handler.includes(`${letter}:`),
`the list names ${letter.toUpperCase()} but the sheet does not handle it`);
}
});