#include "scroller_streams.h" #include "db/database_manager.h" #include #include #include #include namespace { using radiantwave::db::AudioFileRecord; using radiantwave::db::ProclamationRecord; using radiantwave::scroller::ProclamationStream; using radiantwave::scroller::StreamColor; using radiantwave::scroller::buildStreams; using radiantwave::scroller::kDefaultRepeatCount; using radiantwave::scroller::kSelectedRepeatCount; using radiantwave::scroller::minuteInScheduleBlock; const std::string kDefaultId = "default"; std::vector palette() { StreamColor c; c.r = 10; c.g = 20; c.b = 30; return {c}; } } // namespace TEST(ScrollerStreamsTest, InterleavesSelectedWithDefault) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "hello world"; p1.selected = true; p1.solfeggio_frequency_id = 2; ProclamationRecord p2; p2.id = "two"; p2.title = "Two"; p2.content = "foo bar baz"; p2.selected = true; p2.solfeggio_frequency_id = 5; ProclamationRecord def; def.id = kDefaultId; def.title = "Default"; def.content = "default text"; def.selected = true; def.solfeggio_frequency_id = 5; std::unordered_map> palettes; palettes[2] = palette(); std::unordered_map audio; AudioFileRecord af; af.id = 1; af.filename = "396Hz.wav"; af.solfeggio_frequency_id = 2; audio[2] = af; auto streams = buildStreams({p1, p2, def}, palettes, palette(), audio, kDefaultId); // The first user-selected proclamation plays first, followed by the // default, then the next selected, then the default again. ASSERT_EQ(streams.size(), 4u); EXPECT_EQ(streams[0].name, "One"); ASSERT_EQ(streams[0].words.size(), 2u); EXPECT_EQ(streams[0].words[0], "hello"); EXPECT_EQ(streams[0].words[1], "world"); EXPECT_EQ(streams[0].repeat, kSelectedRepeatCount); EXPECT_EQ(streams[0].solfeggio_frequency_id, 2); EXPECT_EQ(streams[0].audioFilename, "396Hz.wav"); EXPECT_EQ(streams[0].palette.size(), 1u); EXPECT_EQ(streams[1].name, "Default"); EXPECT_EQ(streams[1].repeat, kDefaultRepeatCount); EXPECT_EQ(streams[1].solfeggio_frequency_id, 5); EXPECT_EQ(streams[1].audioFilename, "OriginalAudio.wav"); // no audio for freq 5 EXPECT_EQ(streams[1].palette.size(), 1u); // falls back to 528 palette EXPECT_EQ(streams[2].name, "Two"); EXPECT_EQ(streams[2].repeat, kSelectedRepeatCount); EXPECT_EQ(streams[2].palette.size(), 1u); // falls back to 528 palette EXPECT_EQ(streams[3].name, "Default"); EXPECT_EQ(streams[3].repeat, kDefaultRepeatCount); EXPECT_EQ(streams[3].audioFilename, "OriginalAudio.wav"); } TEST(ScrollerStreamsTest, InterleavedListCyclesBackToFirstSelected) { // Three selected proclamations: the built list must be // [S1, Default, S2, Default, S3, Default]. ScrollerPage advances through // this list with a modulo, so once the trailing default finishes the cycle // restarts at the FIRST selected proclamation — the "always on" default // must never accumulate or take over the queue. ProclamationRecord b; b.id = "B"; b.title = "B"; b.content = "bbbb"; b.selected = true; b.solfeggio_frequency_id = 2; ProclamationRecord c; c.id = "C"; c.title = "C"; c.content = "cccc"; c.selected = true; c.solfeggio_frequency_id = 3; ProclamationRecord d; d.id = "D"; d.title = "D"; d.content = "dddd"; d.selected = true; d.solfeggio_frequency_id = 4; ProclamationRecord def; def.id = kDefaultId; def.title = "Default"; def.content = "dddddddddd"; def.selected = true; def.solfeggio_frequency_id = 5; auto streams = buildStreams({b, def, c, d}, {}, palette(), {}, kDefaultId); ASSERT_EQ(streams.size(), 6u); EXPECT_EQ(streams[0].name, "B"); EXPECT_EQ(streams[1].name, "Default"); EXPECT_EQ(streams[2].name, "C"); EXPECT_EQ(streams[3].name, "Default"); EXPECT_EQ(streams[4].name, "D"); EXPECT_EQ(streams[5].name, "Default"); // Every default copy is independent playback state (fresh counters), so a // copy finishing its passes advances to the next stream rather than // restarting "the default" forever. for (size_t i = 1; i < streams.size(); i += 2) { EXPECT_EQ(streams[i].name, "Default"); EXPECT_EQ(streams[i].completed, 0) << "default copy " << i; EXPECT_EQ(streams[i].wordsPlaced, 0) << "default copy " << i; EXPECT_EQ(streams[i].repeat, kDefaultRepeatCount) << "default copy " << i; } } TEST(ScrollerStreamsTest, DefaultOnlyWhenNothingSelected) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "x"; p1.selected = false; ProclamationRecord def; def.id = kDefaultId; def.title = "Default"; def.content = "y"; def.selected = true; auto streams = buildStreams({p1, def}, {}, palette(), {}, kDefaultId); ASSERT_EQ(streams.size(), 1u); EXPECT_EQ(streams[0].name, "Default"); EXPECT_EQ(streams[0].repeat, kDefaultRepeatCount); } TEST(ScrollerStreamsTest, MissingDefaultStillStreamsSelected) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "x"; p1.selected = true; auto streams = buildStreams({p1}, {}, palette(), {}, kDefaultId); ASSERT_EQ(streams.size(), 1u); EXPECT_EQ(streams[0].name, "One"); EXPECT_EQ(streams[0].repeat, kSelectedRepeatCount); } TEST(ScrollerStreamsTest, MinuteInScheduleBlockDaytime) { // 06:00 (360) to 08:00 (480), half-open. EXPECT_FALSE(minuteInScheduleBlock(359, 360, 480, false)); EXPECT_TRUE(minuteInScheduleBlock(360, 360, 480, false)); EXPECT_TRUE(minuteInScheduleBlock(479, 360, 480, false)); EXPECT_FALSE(minuteInScheduleBlock(480, 360, 480, false)); } TEST(ScrollerStreamsTest, MinuteInScheduleBlockOvernight) { // 21:00 (1260) to 09:00 (540) wraps past midnight. EXPECT_TRUE(minuteInScheduleBlock(1260, 1260, 540, true)); EXPECT_TRUE(minuteInScheduleBlock(1439, 1260, 540, true)); EXPECT_TRUE(minuteInScheduleBlock(0, 1260, 540, true)); EXPECT_TRUE(minuteInScheduleBlock(539, 1260, 540, true)); EXPECT_FALSE(minuteInScheduleBlock(540, 1260, 540, true)); EXPECT_FALSE(minuteInScheduleBlock(1259, 1260, 540, true)); } TEST(ScrollerStreamsTest, ScheduledIdsDriveOrderRegardlessOfSelection) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "x"; p1.selected = false; ProclamationRecord p2; p2.id = "two"; p2.title = "Two"; p2.content = "y"; p2.selected = false; ProclamationRecord def; def.id = kDefaultId; def.title = "Default"; def.content = "z"; // Explicit order two -> one, interleaved with the default. Unknown ids are // skipped; the `selected` flag is ignored. std::vector scheduled = {"two", "missing", "one"}; auto streams = buildStreams({p1, p2, def}, {}, palette(), {}, kDefaultId, &scheduled); ASSERT_EQ(streams.size(), 4u); EXPECT_EQ(streams[0].name, "Two"); EXPECT_EQ(streams[1].name, "Default"); EXPECT_EQ(streams[2].name, "One"); EXPECT_EQ(streams[3].name, "Default"); } TEST(ScrollerStreamsTest, EmptyScheduleFallsBackToDefault) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "x"; p1.selected = true; ProclamationRecord def; def.id = kDefaultId; def.title = "Default"; def.content = "y"; std::vector scheduled; // empty block auto streams = buildStreams({p1, def}, {}, palette(), {}, kDefaultId, &scheduled); ASSERT_EQ(streams.size(), 1u); EXPECT_EQ(streams[0].name, "Default"); } TEST(ScrollerStreamsTest, SplitsGluedSentencePunctuation) { ProclamationRecord p1; p1.id = "one"; p1.title = "One"; p1.content = "state?Create NOW?KING field’s fine (Acts 17:27)I example.com"; p1.selected = true; auto streams = buildStreams({p1}, {}, palette(), {}, kDefaultId); ASSERT_EQ(streams.size(), 1u); const auto& words = streams[0].words; // "?" glued to an uppercase word splits after the punctuation, keeping the // sentence separator attached to the preceding word. EXPECT_EQ(words[0], "state?"); EXPECT_EQ(words[1], "Create"); EXPECT_EQ(words[2], "NOW?"); EXPECT_EQ(words[3], "KING"); // Curly apostrophes are not sentence punctuation and stay inside the word. EXPECT_EQ(words[4], "field’s"); EXPECT_EQ(words[5], "fine"); // Closing paren followed by an uppercase letter splits so the verse // reference and the next word are separate tokens. EXPECT_EQ(words[6], "(Acts"); EXPECT_EQ(words[7], "17:27)"); EXPECT_EQ(words[8], "I"); // A lowercase continuation after a dot is not a new sentence, so this // stays intact. EXPECT_EQ(words[9], "example.com"); }