Files
ytplayer/frontend/lyrics-core.test.js
Jonathan Sykes 2b38717c05 Add admin analytics, metadata collection, and grouped lyric cues
Queue reviewable Whisper drafts from the song list and lyrics editor. Preserve line breaks within one timed cue across editing, saving, reporting, and service views.

Add storage and listening analytics with a durable metadata collector, related-search depth, video limits, thumbnail storage, and a browsable metadata library.
2026-10-03 07:55:33 +08:00

94 lines
3.9 KiB
JavaScript

'use strict';
const { test } = require('node:test');
const assert = require('node:assert');
const LC = require('./lyrics-core');
test('fromText: timed, untimed, sections, cues, tags', () => {
const doc = LC.fromText([
'@ Key G, Capo 2 (Play F) | 70 BPM',
'[0:18] # Verse 1',
'[0:18.00] Worthy of every song',
'an untimed line',
'! Drums in',
'',
].join('\n'));
assert.deepStrictEqual(doc.tags, ['Key G', 'Capo 2 (Play F)', '70 BPM']);
assert.deepStrictEqual(doc.lines, [
{ t: 18, text: 'Verse 1', kind: 'section' },
{ t: 18, text: 'Worthy of every song', kind: 'line' },
{ t: null, text: 'an untimed line', kind: 'line' },
{ t: null, text: 'Drums in', kind: 'cue' },
]);
assert.strictEqual(doc.offset, null);
});
test('fromText: LRC multi-stamps expand and a fully timed sheet is time-sorted', () => {
const doc = LC.fromText('[ti:Song]\n[ar:Band]\n[offset:+500]\n[00:30.00]b\n[00:10.00][00:50.00]chorus');
assert.deepStrictEqual(doc.lines.map((l) => [l.t, l.text]), [[10, 'chorus'], [30, 'b'], [50, 'chorus']]);
assert.strictEqual(doc.offset, -0.5); // LRC +500 ms = sooner = our -0.5 s
});
test('toText round-trips through fromText', () => {
const doc = {
tags: ['Key G'],
lines: [
{ t: 83.456, text: 'line one', kind: 'line' },
{ t: null, text: 'Bridge', kind: 'section' },
{ t: 90, text: 'swell', kind: 'cue' },
],
};
const text = LC.toText(doc);
assert.strictEqual(text, '@ Key G\n[1:23.46] line one\n# Bridge\n[1:30.00] ! swell');
const back = LC.fromText(text);
assert.deepStrictEqual(back.lines.map((l) => l.kind), ['line', 'section', 'cue']);
assert.strictEqual(back.lines[0].t, 83.46);
});
test('activeIndex follows sung lines only and honours the offset', () => {
const lines = [
{ t: 10, text: 'Verse', kind: 'section' },
{ t: 10, text: 'a', kind: 'line' },
{ t: null, text: 'untimed', kind: 'line' },
{ t: 20, text: 'b', kind: 'line' },
{ t: 25, text: 'cue', kind: 'cue' },
];
assert.strictEqual(LC.activeIndex(lines, 5, 0), -1);
assert.strictEqual(LC.activeIndex(lines, 12, 0), 1);
assert.strictEqual(LC.activeIndex(lines, 26, 0), 3);
// +2 s offset: the "b" line shows at 22 s of playback, not 20.
assert.strictEqual(LC.activeIndex(lines, 21, 2), 1);
assert.strictEqual(LC.activeIndex(lines, 22, 2), 3);
assert.strictEqual(LC.activeSection(lines, 12, 0).text, 'Verse');
assert.strictEqual(LC.timedCount(lines), 2);
});
test('clock and stamp formatting', () => {
assert.strictEqual(LC.fmtClock(83.9), '1:23');
assert.strictEqual(LC.fmtClock(3723), '1:02:03');
assert.strictEqual(LC.fmtStamp(5.5), '0:05.50');
assert.strictEqual(LC.parseStamp('1:23.5'), 83.5);
assert.strictEqual(LC.parseStamp('1:02:03'), 3723);
assert.strictEqual(LC.parseStamp('nope'), null);
});
test('YouTube export adds a 0:00 entry only when needed', () => {
const items = [{ t: 130, title: 'Key change → D' }, { t: 42, title: 'Intro' }];
assert.strictEqual(LC.toYouTubeTimestamps(items), '0:00 Start\n0:42 Intro\n2:10 Key change → D');
assert.strictEqual(LC.toYouTubeTimestamps([{ t: 0, title: 'Top' }]), '0:00 Top');
assert.strictEqual(LC.activeMarker(items, 60).title, 'Intro');
assert.strictEqual(LC.activeMarker(items, 10), null);
});
test('multiline cues round-trip without adding timestamps or active rows', () => {
const doc = { lines: [{ t: 12, text: 'Because You are God\nYou can do anything', kind: 'line' }, { t: 18, text: 'Next lyric', kind: 'line' }], tags: [] };
const copy = LC.fromText(LC.toText(doc));
assert.deepStrictEqual(copy.lines, doc.lines);
assert.strictEqual(LC.activeIndex(copy.lines, 15, 0), 0);
assert.strictEqual(LC.activeIndex(copy.lines, 18, 0), 1);
assert.strictEqual(LC.timedCount(copy.lines), 2);
const repeated = LC.fromText('[0:12][0:30] Because You are God\n| You can do anything');
assert.strictEqual(repeated.lines.length, 2);
assert.strictEqual(repeated.lines[1].text, doc.lines[0].text);
});