Derived from the clock rather than a literal, so a build shipped next year does not display a stale date. The About test asserts the rendered string, year included.
256 lines
11 KiB
C#
256 lines
11 KiB
C#
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;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// The About overlay is the in-app creator hub: it opens from the real window,
|
|
/// and its licensing sub-panel surfaces the full THIRD-PARTY-NOTICES.txt
|
|
/// in-app (scrolling text, never the OS viewer / Notepad). Drives the real VM
|
|
/// through the real window — same real-App + temp-DB pattern as the round-clip
|
|
/// and source-naming tests.
|
|
/// </summary>
|
|
[Collection("RealApp")]
|
|
public sealed class AboutHubTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public AboutHubTests(RealAppHost app)
|
|
{
|
|
_app = app;
|
|
}
|
|
|
|
[Fact]
|
|
public void About_Opens_InApp_Licensing_Loads_Shipped_Notices()
|
|
{
|
|
_app.Run(Run);
|
|
}
|
|
|
|
private void Run()
|
|
{
|
|
var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-about-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
var window = new MainWindow();
|
|
var vm = (MainViewModel)window.DataContext;
|
|
try
|
|
{
|
|
// About hub opens (the wordmark / gear menu entry point).
|
|
vm.OpenAboutCommand.Execute(null);
|
|
Assert.True(vm.IsAboutOpen);
|
|
Assert.False(vm.IsLicensesOpen);
|
|
|
|
// The hub links resolve to real destinations.
|
|
Assert.Equal("https://youtube.com/@llamachileshop", MainViewModel.ChannelUrl);
|
|
Assert.Equal("https://mastodon.llamachile.tube/@gramps", MainViewModel.MastodonUrl);
|
|
Assert.Equal("https://buymeacoffee.com/llamachiley", MainViewModel.CoffeeUrl);
|
|
Assert.Equal("https://llamacasty.com", MainViewModel.PremiumUrl);
|
|
|
|
// The licensing panel loads the full shipped notices text in-app.
|
|
vm.OpenLicensesCommand.Execute(null);
|
|
Assert.True(vm.IsLicensesOpen);
|
|
Assert.False(string.IsNullOrWhiteSpace(vm.LicensesText));
|
|
Assert.Contains("Third-Party Notices", vm.LicensesText, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Contains("LGPL", vm.LicensesText, StringComparison.OrdinalIgnoreCase);
|
|
|
|
// Back returns to the hub view (code-behind click handler path is
|
|
// equivalent; here we assert the VM contract the handler uses).
|
|
vm.IsLicensesOpen = false;
|
|
Assert.True(vm.IsAboutOpen);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { System.IO.File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The About box is a conversion surface (creator, 2026-11-05): a large logo, the 1-2-3 tagline,
|
|
/// the offer, one primary CTA with the price on it — and a paying customer sees none of the price.
|
|
/// </summary>
|
|
[Fact]
|
|
public void About_ShowsTheOfferToFreeUsers_AndNothingToSellToOwners()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-about-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
MainWindow? window = null;
|
|
try
|
|
{
|
|
window = new MainWindow();
|
|
var vm = (MainViewModel)window.DataContext;
|
|
vm.EntitlementOverride = new FakeEntitlement(false, "Steam licensing is not configured in this build.");
|
|
window.Show();
|
|
vm.OpenAboutCommand.Execute(null);
|
|
window.UpdateLayout();
|
|
|
|
var badge = FindVisualChildByName<Button>(window, "ReleaseBadgeButton");
|
|
Assert.NotNull(badge);
|
|
Assert.Equal("Pre-Release", badge!.Content);
|
|
Assert.NotNull(badge.Command); // the badge links out until the Steam page exists
|
|
|
|
var tagline = FindVisualChildByName<TextBlock>(window, "AboutTagline");
|
|
Assert.NotNull(tagline);
|
|
Assert.Equal("1-2-3, and go live to the world on YouTube!", tagline!.Text);
|
|
// The old line insulted the creator and is gone for good.
|
|
Assert.DoesNotContain("right-brained", TextOf(window));
|
|
|
|
var price = FindVisualChildByName<TextBlock>(window, "AboutPriceLine");
|
|
Assert.Equal("Launch price $79", price!.Text);
|
|
|
|
var cta = FindVisualChildByName<Button>(window, "UpgradeCtaButton");
|
|
Assert.NotNull(cta);
|
|
Assert.Equal("Unlock 60 fps — $79", cta!.Content);
|
|
var upgradeBlock = FindVisualChildByName<StackPanel>(window, "UpgradeBlock");
|
|
Assert.Equal(Visibility.Visible, upgradeBlock!.Visibility);
|
|
|
|
var copyright = FindVisualChildByName<TextBlock>(window, "AboutCopyright");
|
|
Assert.Equal($"© {DateTime.Now.Year} Llama Chile Shop", copyright!.Text);
|
|
Assert.DoesNotContain("Made by gramps", TextOf(window));
|
|
|
|
// A paying customer is never sold to.
|
|
vm.EntitlementOverride = new FakeEntitlement(true);
|
|
window.UpdateLayout();
|
|
Assert.Equal(Visibility.Collapsed, FindVisualChildByName<StackPanel>(window, "UpgradeBlock")!.Visibility);
|
|
var owned = FindVisualChildByName<TextBlock>(window, "OwnedStateText");
|
|
Assert.Equal(Visibility.Visible, owned!.Visibility);
|
|
Assert.Contains("owned", owned.Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
finally
|
|
{
|
|
window?.Close();
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Free_user_gets_1080p30_and_cannot_select_or_change_to_60fps()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"llamacasty-upgrade-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
MainWindow? window = null;
|
|
try
|
|
{
|
|
window = new MainWindow();
|
|
var vm = (MainViewModel)window.DataContext;
|
|
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);
|
|
|
|
// The old red panel under the quality picker is gone (2026-11-05): the licence state
|
|
// lives in the left panel's alert stack now, as a pinned status indicator.
|
|
Assert.Null(window.FindName("EntitlementErrorPanel"));
|
|
var licenceRow = vm.AttentionAlerts.Single(alert => alert.Id == "steam-licence");
|
|
Assert.True(licenceRow.IsPinned);
|
|
Assert.Contains("Steam ID Limit: 30fps", licenceRow.Text);
|
|
Assert.Contains("Click for More Info", licenceRow.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.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.False(vm.IsAboutOpen);
|
|
|
|
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
|
|
{
|
|
window?.Close();
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { System.IO.File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
});
|
|
}
|
|
|
|
private static T? FindVisualChildByName<T>(DependencyObject parent, string name) where T : DependencyObject
|
|
{
|
|
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is FrameworkElement element && element.Name == name && child is T typed) return typed;
|
|
if (FindVisualChildByName<T>(child, name) is { } found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Every TextBlock string in the tree — for asserting that removed copy is really gone.</summary>
|
|
private static string TextOf(DependencyObject parent)
|
|
{
|
|
var text = new System.Text.StringBuilder();
|
|
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
|
{
|
|
var child = VisualTreeHelper.GetChild(parent, i);
|
|
if (child is TextBlock block) text.Append(block.Text).Append('\n');
|
|
text.Append(TextOf(child));
|
|
}
|
|
return text.ToString();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|