feat(entitlement): gate 60 fps output through Steam

This commit is contained in:
2026-11-04 12:14:46 -08:00
parent 1993e7cc8b
commit 7216c95449
12 changed files with 188 additions and 72 deletions
+54 -17
View File
@@ -2,9 +2,11 @@ using System;
using System.Windows;
using Microsoft.Data.Sqlite;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using Xunit;
using ytLive.Controls;
using ytLive.Models;
using ytLive.Services.Entitlement;
using ytLive.ViewModels;
@@ -74,7 +76,7 @@ public sealed class AboutHubTests
}
[Fact]
public void Selecting_a_locked_quality_opens_about_and_keeps_the_free_tier_selected()
public void Free_user_gets_1080p30_and_cannot_select_or_change_to_60fps()
{
_app.Run(() =>
{
@@ -85,36 +87,59 @@ public sealed class AboutHubTests
{
window = new MainWindow();
var vm = (MainViewModel)window.DataContext;
vm.EntitlementOverride = new FakeEntitlement(false);
var freeTier = vm.QualityOptions.Single(option => !option.RequiresPro);
var lockedTier = vm.QualityOptions.First(option => option.RequiresPro);
vm.SelectedQuality = freeTier;
vm.EntitlementOverride = new FakeEntitlement(false,
"Steam could not verify ownership. Open Steam, sign in, then restart LlamaCasty.");
window.Show();
window.UpdateLayout();
var freeTier = vm.QualityOptions.Single(option => option.Display == "1080p30");
var lockedTier = vm.QualityOptions.First(option => option.Fps == 60);
Assert.Same(freeTier, vm.SelectedQuality);
var errorPanel = (Border)window.FindName("EntitlementErrorPanel");
var errorText = (TextBlock)window.FindName("EntitlementErrorText");
Assert.Equal(Visibility.Visible, errorPanel.Visibility);
Assert.Contains("Open Steam, sign in", errorText.Text);
var picker = (ComboBox)window.FindName("QualityPicker");
Assert.True(vm.CanChangeQuality);
Assert.True(picker.IsEnabled);
picker.IsDropDownOpen = true;
picker.UpdateLayout();
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
var lockedItem = (ComboBoxItem)picker.ItemContainerGenerator.ContainerFromItem(lockedTier)!;
Assert.True(lockedItem.IsEnabled);
Assert.Equal(0.55, lockedItem.Opacity);
Assert.Equal(Application.Current.Resources["Brush.OnSurfaceDisabled"], lockedItem.Foreground);
Assert.False(lockedItem.IsEnabled);
var struckText = FindVisualChild<TextBlock>(lockedItem);
Assert.NotNull(struckText);
Assert.Contains(struckText!.TextDecorations, decoration =>
decoration.Location == TextDecorationLocation.Strikethrough);
picker.SelectedItem = lockedTier;
picker.UpdateLayout();
Assert.Same(freeTier, vm.SelectedQuality);
Assert.True(vm.IsAboutOpen);
Assert.False(vm.IsAboutOpen);
var about = (OverlayHost)window.FindName("AboutOverlay");
var copy = (TextBlock)about.FindName("UpgradeCopy");
var link = (Button)about.FindName("UpgradeLinkButton");
Assert.Contains("720p30", copy.Text);
Assert.Contains("one-time purchase", copy.Text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("no subscription", copy.Text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("llamacasty.com", link.Content!.ToString());
Assert.Same(vm.OpenPremiumCommand, link.Command);
picker.IsDropDownOpen = false;
vm.IsRecording = true;
window.UpdateLayout();
Assert.False(vm.CanChangeQuality);
Assert.False(picker.IsEnabled);
vm.SelectedQuality = vm.QualityOptions.Single(option => option.Display == "720p30");
Assert.Same(freeTier, vm.SelectedQuality);
vm.IsRecording = false;
vm.StreamStatus = StreamStatus.Streaming;
window.UpdateLayout();
Assert.False(vm.CanChangeQuality);
Assert.False(picker.IsEnabled);
vm.SelectedQuality = vm.QualityOptions.Single(option => option.Display == "720p30");
Assert.Same(freeTier, vm.SelectedQuality);
vm.StreamStatus = StreamStatus.Offline;
Assert.True(vm.CanChangeQuality);
Assert.True(picker.IsEnabled);
}
finally
{
@@ -125,4 +150,16 @@ public sealed class AboutHubTests
}
});
}
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T match) return match;
var nested = FindVisualChild<T>(child);
if (nested != null) return nested;
}
return null;
}
}