diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index 021b574..65d5a4c 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -460,6 +460,12 @@ + + diff --git a/Models/ScoreboardSample.cs b/Models/ScoreboardSample.cs new file mode 100644 index 0000000..e2f45b2 --- /dev/null +++ b/Models/ScoreboardSample.cs @@ -0,0 +1,29 @@ +namespace ytLive.Models; + +/// Development-only sample data for the scoreboard. +/// +/// Creator (2026-10-30): the strip must be provable in the PREVIEW before a real stream — +/// "simulate the ticker so we can see it and ensure it's getting composited". Synthetic events go +/// through the same a live stream uses, so what the preview shows +/// is exactly what real events would produce; nothing here fakes the renderer. +/// +/// Kept in the model (not the view-model) so the sample is testable without a window. +public static class ScoreboardSample +{ + public static void Inject(ScoreboardRecorder recorder) + { + if (!recorder.IsRunning) recorder.StartSession(); + + recorder.Record(new ChatMessage { Kind = ChatEventKind.NewMember, AuthorName = "newviewer" }); + recorder.Record(new ChatMessage + { + Kind = ChatEventKind.MemberGift, AuthorName = "giftgiver", GiftNewMembersCount = 3, + }); + recorder.Record(new ChatMessage { Kind = ChatEventKind.MemberMilestone, AuthorName = "longtimer" }); + recorder.Record(new ChatMessage + { + Kind = ChatEventKind.SuperChat, AuthorName = "tipper", + SuperChatAmount = 12.34, SuperChatCurrency = "USD", SuperChatDisplayString = "$12.34", + }); + } +} diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs index 30d0aba..3bedacb 100644 --- a/ViewModels/MainViewModel.Scoreboard.cs +++ b/ViewModels/MainViewModel.Scoreboard.cs @@ -45,6 +45,33 @@ public partial class MainViewModel } } +#if DEBUG + private const bool DevelopmentBuild = true; +#else + private const bool DevelopmentBuild = false; +#endif + + /// True in a DEBUG build. Used only to expose development-only controls, never to + /// change behaviour — the shipped binary differs by their absence, not by a code path. + public bool IsDevelopmentBuild => DevelopmentBuild; + + private bool _scoreboardSimulate; + + /// DEBUG-only: push a representative set of events so the strip is visible in the + /// preview before any real stream. Off clears back to an empty session. + public bool ScoreboardSimulate + { + get => _scoreboardSimulate; + set + { + if (!SetProperty(ref _scoreboardSimulate, value)) return; + if (value) ScoreboardSample.Inject(_scoreboard); + else _scoreboard.StartSession(); // empty session: "nothing yet" + RefreshScoreboardPreviews(); + OnPropertyChanged(nameof(ScoreboardFrame)); + } + } + /// What the cadence policy asks: the master switch AND at least one scene showing it. internal bool ScoreboardPollsEnabled => _scoreboardEnabled && Scenes.Any(scene => diff --git a/ytLive.Tests/ScoreboardSimulationTests.cs b/ytLive.Tests/ScoreboardSimulationTests.cs new file mode 100644 index 0000000..9a521a6 --- /dev/null +++ b/ytLive.Tests/ScoreboardSimulationTests.cs @@ -0,0 +1,46 @@ +using Xunit; +using ytLive.Models; +using ytLive.ViewModels; + +namespace ytLive.Tests; + +/// DEBUG-only affordance (creator 2026-10-30): push sample events so the strip is provable +/// in the PREVIEW before a real stream. The sample must travel the real recorder, so what it paints +/// is what live events paint — a fake renderer would prove nothing. +public class ScoreboardSimulationTests +{ + [Fact] + public void The_dev_sample_feeds_the_real_recorder_and_the_toggle_is_debug_only() + { + // The sample goes through the recorder a stream uses, and lands all four tallies. + var recorder = new ScoreboardRecorder(); + ScoreboardSample.Inject(recorder); + + Assert.True(recorder.IsRunning); + var state = recorder.State; + Assert.Equal(4, state.NewMembers); // 1 paid + a gift of 3 + Assert.Equal(3, state.LastGiftCount); + Assert.Equal("giftgiver", state.LastGifterName); + Assert.Equal(1, state.MilestoneCount); + Assert.Equal(12.34, state.SessionTotal.Total, 2); + Assert.Equal("$12.34", state.LastSuperChatDisplay); + + // A toggle OFF clears back to an empty session (so the empty state is visible too). + recorder.StartSession(); + Assert.Equal(0, recorder.State.NewMembers); + + // The control exists and is gated on the build, not on a runtime flag. + var panel = SourceAssert.Text("Controls", "LeftPanel.xaml"); + Assert.Contains("ScoreboardSimulate", panel); + Assert.Contains("IsDevelopmentBuild", panel); + + var type = typeof(MainViewModel); + Assert.NotNull(type.GetProperty("ScoreboardSimulate")); + Assert.NotNull(type.GetProperty("IsDevelopmentBuild")); + // DEBUG-gated at compile time, so a Release build has no dev control at all. + var vmSource = SourceAssert.Text("ViewModels", "MainViewModel.Scoreboard.cs"); + Assert.Contains("#if DEBUG", vmSource); + Assert.Contains("private const bool DevelopmentBuild = true;", vmSource); + Assert.Contains("private const bool DevelopmentBuild = false;", vmSource); + } +}