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.
73 lines
3.0 KiB
JavaScript
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`);
|
|
}
|
|
});
|