ScoreboardState folds the six live-chat event kinds into the numbers a creator puts on screen: last member, last Super Chat, last gift, members gained, milestones, and session money per currency. Scope is the running session (creator ruling): reset at start, dead at End, no persistence. Two rules are load-bearing: - money is keyed by currency and never summed across them; the tile shows the dominant currency by VALUE, so a single EUR tip cannot flip the display away from the larger USD total - a gifted membership is counted ONCE, from membershipGiftingEvent (which carries the count); giftMembershipReceivedEvent is the recipient's view of the same purchase and changes nothing. The test fires five recipient events after one purchase of five and the total must not move.
69 lines
3.1 KiB
C#
69 lines
3.1 KiB
C#
using Xunit;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>Session-scoped scoreboard tallies. The rules that matter: money is never added across
|
|
/// currencies, and a gifted membership is counted ONCE (from the purchase), never again from the
|
|
/// recipient's event — that double-count is the easy mistake, and the creator called it out.</summary>
|
|
public class ScoreboardStateTests
|
|
{
|
|
private static ChatMessage Message(ChatEventKind kind, string author = "viewer", double amount = 0,
|
|
string? currency = "USD", int giftCount = 0, string? display = null) => new()
|
|
{
|
|
Kind = kind, AuthorName = author, SuperChatAmount = amount,
|
|
SuperChatCurrency = currency, GiftNewMembersCount = giftCount, SuperChatDisplayString = display,
|
|
};
|
|
|
|
[Fact]
|
|
public void Tallies_follow_the_six_events_and_never_double_count_a_gift()
|
|
{
|
|
var state = new ScoreboardState();
|
|
|
|
state.Apply(Message(ChatEventKind.NewMember, "Paid"));
|
|
Assert.Equal(1, state.NewMembers);
|
|
Assert.Equal("Paid", state.LastMemberName);
|
|
|
|
// A gift purchase of 5 memberships counts 5...
|
|
state.Apply(Message(ChatEventKind.MemberGift, "Gifter", giftCount: 5));
|
|
Assert.Equal(6, state.NewMembers);
|
|
Assert.Equal(5, state.LastGiftCount);
|
|
Assert.Equal("Gifter", state.LastGifterName);
|
|
|
|
// ...and the recipients' own events add NOTHING (they are the same memberships).
|
|
for (var i = 0; i < 5; i++) state.Apply(Message(ChatEventKind.GiftReceived, $"Recipient{i}", giftCount: 1));
|
|
Assert.Equal(6, state.NewMembers);
|
|
Assert.Equal(5, state.LastGiftCount);
|
|
Assert.Equal("Gifter", state.LastGifterName);
|
|
|
|
state.Apply(Message(ChatEventKind.MemberMilestone, "Longtimer"));
|
|
Assert.Equal(1, state.MilestoneCount);
|
|
|
|
// Super Chat and Super Sticker both carry money; the last one drives the "latest" tile.
|
|
state.Apply(Message(ChatEventKind.SuperChat, "Tipper", 10, "USD", display: "$10.00"));
|
|
state.Apply(Message(ChatEventKind.SuperSticker, "Sticker", 5, "USD"));
|
|
Assert.Equal(15, state.SessionTotal.Total, 3);
|
|
Assert.Equal("USD", state.SessionTotal.Currency);
|
|
Assert.Equal("Sticker", state.LastSuperChatAuthor);
|
|
Assert.False(state.HasMixedCurrencies);
|
|
|
|
// A second currency is tracked separately, never summed into the first.
|
|
state.Apply(Message(ChatEventKind.SuperChat, "Euro", 3, "EUR"));
|
|
Assert.Equal(15, state.MoneyByCurrency["USD"], 3);
|
|
Assert.Equal(3, state.MoneyByCurrency["EUR"], 3);
|
|
Assert.True(state.HasMixedCurrencies);
|
|
Assert.Equal("USD", state.SessionTotal.Currency); // dominant by value, not by recency
|
|
|
|
// Plain chat is ignored entirely.
|
|
state.Apply(Message(ChatEventKind.None, "Chatter"));
|
|
Assert.Equal(6, state.NewMembers);
|
|
Assert.Equal(1, state.MilestoneCount);
|
|
|
|
// A new session starts clean.
|
|
state.Reset();
|
|
Assert.Equal(0, state.NewMembers);
|
|
Assert.Equal(0, state.SessionTotal.Total);
|
|
Assert.Null(state.LastMemberName);
|
|
}
|
|
}
|