feat(scoreboard): a DEBUG-only "Simulate" switch, so the strip is provable in the preview

Creator (2026-10-30): pre-1.0, use DEBUG — add a toggle that simulates the ticker so the strip can
be SEEN and its compositing verified before a real stream.

The sample travels the REAL ScoreboardRecorder a stream uses (one new member, a gift of three, a
milestone, a $12.34 Super Chat), so what the preview shows is exactly what live events produce — a
fake renderer would prove nothing. Toggling off clears to an empty session, so the empty state is
visible too.

Debug-gated at COMPILE time: `DevelopmentBuild` is a const true in DEBUG / false in Release, and the
checkbox binds its Visibility to it. A shipped Release build therefore has no dev control at all,
rather than one hidden by a runtime flag.

Guard: ScoreboardSimulationTests — the sample lands all four tallies through the recorder, off clears,
and the panel + const pair exist in the Dual (DEBUG/Release) form.
This commit is contained in:
2026-10-30 14:58:56 -07:00
parent ea1017776d
commit e6abc13461
4 changed files with 108 additions and 0 deletions
+6
View File
@@ -460,6 +460,12 @@
<TextBlock Text="Sign in to YouTube to use the scoreboard."
Foreground="{StaticResource Brush.OnSurfaceVariant}" FontSize="11" Margin="0,0,0,4"
Visibility="{Binding IsConnected, Converter={StaticResource InverseBoolToVis}}"/>
<!-- DEBUG builds only: prove the strip in the preview before a real
stream. Absent from Release, not merely hidden by a flag. -->
<CheckBox Style="{StaticResource YtCheckBox}" Margin="0,0,0,4"
Content="Simulate (dev)" IsChecked="{Binding ScoreboardSimulate, Mode=TwoWay}"
ToolTip="Push sample events so the strip appears in the preview."
Visibility="{Binding IsDevelopmentBuild, Converter={StaticResource BoolToVis}}"/>
<CheckBox Style="{StaticResource YtCheckBox}" Margin="0,0,0,4"
Content="Show the scoreboard" IsChecked="{Binding ScoreboardEnabled, Mode=TwoWay}"
ToolTip="Off hides the strip in every scene AND stops the channel polling."/>
+29
View File
@@ -0,0 +1,29 @@
namespace ytLive.Models;
/// <summary>Development-only sample data for the scoreboard.
///
/// <para>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 <see cref="ScoreboardRecorder"/> a live stream uses, so what the preview shows
/// is exactly what real events would produce; nothing here fakes the renderer.</para>
///
/// <para>Kept in the model (not the view-model) so the sample is testable without a window.</para></summary>
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",
});
}
}
+27
View File
@@ -45,6 +45,33 @@ public partial class MainViewModel
}
}
#if DEBUG
private const bool DevelopmentBuild = true;
#else
private const bool DevelopmentBuild = false;
#endif
/// <summary>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.</summary>
public bool IsDevelopmentBuild => DevelopmentBuild;
private bool _scoreboardSimulate;
/// <summary>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.</summary>
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));
}
}
/// <summary>What the cadence policy asks: the master switch AND at least one scene showing it.</summary>
internal bool ScoreboardPollsEnabled =>
_scoreboardEnabled && Scenes.Any(scene =>
+46
View File
@@ -0,0 +1,46 @@
using Xunit;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>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.</summary>
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);
}
}