Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,27 @@
IsVisible="{Binding IsLoading}"
Text="{Binding LoadingText}" />

<ItemsControl ItemsSource="{Binding Items}" IsVisible="{Binding HasItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
Margin="0,0,0,8"
Command="{Binding Command}"
Info="{Binding CombinedInfo}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ScrollViewer
MaxHeight="420"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Items}" IsVisible="{Binding HasItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
Margin="0,0,0,8"
Command="{Binding Command}"
Info="{Binding CombinedInfo}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</ui:PclCard.CardContent>
</ui:PclCard>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,25 +315,30 @@
Text="{Binding FilteredEmptyStateText}"
TextWrapping="Wrap" />

<ItemsControl ItemsSource="{Binding VisibleChoices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
HorizontalAlignment="Stretch"
Margin="0"
Command="{Binding SelectCommand}"
IconSource="{Binding Icon}"
Info="{Binding Summary}"
IsSelected="{Binding IsSelected}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ScrollViewer
MaxHeight="420"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding VisibleChoices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
HorizontalAlignment="Stretch"
Margin="0"
Command="{Binding SelectCommand}"
IconSource="{Binding Icon}"
Info="{Binding Summary}"
IsSelected="{Binding IsSelected}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>

</StackPanel>
</ui:PclCard.CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,31 @@
Text="{Binding FilteredEmptyStateText}"
TextWrapping="Wrap" />

<ItemsControl ItemsSource="{Binding VisibleChoices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
HorizontalAlignment="Stretch"
Margin="0"
Command="{Binding SelectCommand}"
IconSource="{Binding Icon}"
Info="{Binding Summary}"
IsSelected="{Binding IsSelected}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ScrollViewer
x:Name="OptionChoicesScroll"
MaxHeight="420"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding VisibleChoices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel CacheLength="0.5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:PclListItem
HorizontalAlignment="Stretch"
Margin="0"
Command="{Binding SelectCommand}"
IconSource="{Binding Icon}"
Info="{Binding Summary}"
IsSelected="{Binding IsSelected}"
Title="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</ui:PclCard.CardContent>
</ui:PclCard>
Expand Down
27 changes: 27 additions & 0 deletions src/PCL.Frontend.Avalonia/Workflows/FrontendSystemMemoryService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using PCL.Core.Utils.OS;

namespace PCL.Frontend.Avalonia.Workflows;

Expand All @@ -12,6 +13,10 @@ internal static class FrontendSystemMemoryService
private static DateTimeOffset _cachedMemoryStateTime;
private static (double TotalGb, double AvailableGb) _cachedMemoryState;

// Replaces the platform reader in tests so the GB conversion and the preference for a
// real platform source over the GC fallback can be verified on any OS.
internal static Func<(long TotalBytes, long AvailableBytes)>? PhysicalMemoryBytesOverride { get; set; }

public static bool TryGetCachedPhysicalMemoryState(out (double TotalGb, double AvailableGb) memoryState)
{
var now = DateTimeOffset.UtcNow;
Expand Down Expand Up @@ -39,6 +44,15 @@ public static (double TotalGb, double AvailableGb) GetFallbackPhysicalMemoryStat
return ToGigabytes(totalBytes, availableBytes);
}

internal static void InvalidateMemoryStateCache()
{
lock (MemoryStateCacheLock)
{
_cachedMemoryState = default;
_cachedMemoryStateTime = default;
}
}

public static (double TotalGb, double AvailableGb) GetPhysicalMemoryState()
{
var now = DateTimeOffset.UtcNow;
Expand Down Expand Up @@ -168,6 +182,19 @@ private static (long TotalBytes, long AvailableBytes) TryReadPhysicalMemoryBytes
{
try
{
if (PhysicalMemoryBytesOverride is { } overrideSource)
{
return overrideSource();
}

if (OperatingSystem.IsWindows())
{
// GlobalMemoryStatusEx reports real system-wide usage; the GC fallback
// below only knows about this process's managed heap.
var memory = KernelInterop.GetPhysicalMemoryBytes();
return ((long)memory.Total, (long)memory.Available);
}

if (OperatingSystem.IsLinux())
{
return TryReadLinuxPhysicalMemoryBytes();
Expand Down
78 changes: 78 additions & 0 deletions tests/PCL.Frontend.Avalonia.E2E/DownloadCatalogE2E.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Reflection;
using Avalonia.Controls;
using Avalonia.VisualTree;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PCL.Frontend.Avalonia.Desktop.Panes.Right;
using PCL.Frontend.Avalonia.ViewModels;
using static PCL.Frontend.Avalonia.E2E.DesktopFixture;

namespace PCL.Frontend.Avalonia.E2E;

[TestClass]
[DoNotParallelize]
public sealed class DownloadCatalogE2E
{
private const double BoundedChoiceListMaxHeight = 420;

[TestMethod]
public Task InstallerCatalog_LongVersionListScrollsInsideABoundedCard() => Run(async () =>
{
await using var app = new DesktopFixture();
await app.Start();
await app.Session.NavigateAsync("Download/DownloadForge");
await Wait(() => app.Window.GetVisualDescendants().OfType<DownloadCatalogRightPaneView>().Any(), "The installer catalog did not open.");
var view = app.Window.GetVisualDescendants().OfType<DownloadCatalogRightPaneView>().Single();
SeedForgeVersionSection(app.Launcher);
PclCard? SectionCard() => view.GetVisualDescendants().OfType<PclCard>()
.SingleOrDefault(section => section.DataContext is DownloadCatalogSectionViewModel);
await Wait(() => SectionCard() is { IsEffectivelyVisible: true }, "The seeded installer section did not appear.");
var card = SectionCard()!;
await Click(app.Window, card.Find<Button>("HeaderButton")!);
await Wait(() => card.DataContext is DownloadCatalogSectionViewModel { IsExpanded: true } &&
card.GetVisualDescendants().OfType<PclListItem>().Any(), "Catalog versions did not appear.");
await Settle();

var scroller = card.GetVisualDescendants().OfType<ScrollViewer>()
.Single(scroll => scroll.Content is ItemsControl { ItemCount: 120 });
Assert.AreEqual(BoundedChoiceListMaxHeight, scroller.MaxHeight, 0.01);
Assert.IsTrue(scroller.Extent.Height > scroller.Viewport.Height * 4,
$"The version list must scroll internally instead of stretching the card (extent {scroller.Extent.Height}, viewport {scroller.Viewport.Height}).");
Assert.IsTrue(scroller.Viewport.Height <= BoundedChoiceListMaxHeight + 0.5,
$"The version list viewport must stay bounded (was {scroller.Viewport.Height}).");
Assert.IsTrue(card.Bounds.Height < 600,
$"An expanded installer section must not fill the page (height {card.Bounds.Height}).");
Assert.IsTrue(scroller.GetVisualDescendants().OfType<PclListItem>().Count() < 30,
"Long version lists must remain virtualized inside the bounded card.");
app.Screenshot("download-catalog-forge-expanded");

scroller.ScrollToEnd();
await Settle();
var lastVersion = scroller.GetVisualDescendants().OfType<PclListItem>().Single(item => item.Title == "1.20.1 - 119");
await Click(app.Window, lastVersion);

await Click(app.Window, card.Find<Button>("HeaderButton")!);
await Wait(() => card.Bounds.Height <= 45, "The installer section did not collapse back to its header height.");
app.Screenshot("download-catalog-forge-collapsed");
});

private static void SeedForgeVersionSection(LauncherViewModel launcher)
{
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
// Invalidate the in-flight network refresh so it cannot replace the synthetic section.
var version = typeof(LauncherViewModel).GetField("_downloadCatalogRefreshVersion", flags)!;
version.SetValue(launcher, (int)version.GetValue(launcher)! + 1);
typeof(LauncherViewModel).GetMethod("SetDownloadCatalogLoading", flags)!
.Invoke(launcher, [false]);
var command = new ActionCommand(() => { });
launcher.DownloadCatalogSections.Add(new DownloadCatalogSectionViewModel(
"1.20.1 (120)",
Enumerable.Range(0, 120).Select(index => new DownloadCatalogEntryViewModel(
$"1.20.1 - {index}",
$"47.2.{index}",
string.Empty,
"save_installer",
command)).ToArray(),
isCollapsible: true,
isExpanded: false));
}
}
102 changes: 102 additions & 0 deletions tests/PCL.Frontend.Avalonia.E2E/InstallOptionsE2E.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Reflection;
using System.Text.Json.Nodes;
using Avalonia.Controls;
using Avalonia.VisualTree;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PCL.Frontend.Avalonia.Desktop.Controls;
using PCL.Frontend.Avalonia.Desktop.Panes.Right;
using PCL.Frontend.Avalonia.ViewModels;
using PCL.Frontend.Avalonia.Workflows;
using PCL.UI.Controls;
using static PCL.Frontend.Avalonia.E2E.DesktopFixture;

namespace PCL.Frontend.Avalonia.E2E;

[TestClass]
[DoNotParallelize]
public sealed class InstallOptionsE2E
{
private const double BoundedChoiceListMaxHeight = 420;

[TestMethod]
public Task OptionCard_LongChoiceListScrollsInsideABoundedCard() => Run(async () =>
{
await using var app = new DesktopFixture();
await app.Start();
SeedCatalog(app.Launcher);
await Click(app.Window, app.Window.GetVisualDescendants().OfType<PclTabButton>()
.Single(tab => tab.IsEffectivelyVisible && tab.Text == "Download"));
await Wait(() => app.Window.GetVisualDescendants().OfType<DownloadInstallRightPaneView>().Any(), "Version catalog did not open.");
var view = app.Window.GetVisualDescendants().OfType<DownloadInstallRightPaneView>().Single();
var field = view.GetVisualDescendants().OfType<PclSearchBox>().Single().Find<TextBox>("SearchTextBox")!;
await Type(app.Window, field, "1.21.119");
var list = view.Find<ItemsControl>("MinecraftCatalogExpandedList")!;
await Wait(() => view.Find<PclCard>("MinecraftCatalogExpandedHost")!.IsVisible && list.ItemCount == 1, "Exact version search did not update.");
await Settle();
await Click(app.Window, list.GetVisualDescendants().OfType<PclListItem>().Single(item => item.Title == "1.21.119"));
await Wait(() => app.Launcher.ShowDownloadInstallSelectionStage, "Selecting a version did not open installation options.");

SeedForgeChoices(app.Launcher, 120);

PclCard ForgeCard() => view.GetVisualDescendants().OfType<PclCard>()
.Single(card => card.DataContext is DownloadInstallOptionCardViewModel { Title: "Forge" });
await Click(app.Window, ForgeCard().Find<Button>("HeaderButton")!);
var card = ForgeCard();
await Wait(() => card.DataContext is DownloadInstallOptionCardViewModel { IsExpanded: true } &&
card.GetVisualDescendants().OfType<PclListItem>().Any(), "Forge choices did not appear.");
await Settle();

var scroller = card.GetVisualDescendants().OfType<ScrollViewer>()
.Single(scroll => scroll.Content is ItemsControl { ItemCount: 120 });
Assert.AreEqual(BoundedChoiceListMaxHeight, scroller.MaxHeight, 0.01);
Assert.IsTrue(scroller.Extent.Height > scroller.Viewport.Height * 4,
$"The choice list must scroll internally instead of stretching the card (extent {scroller.Extent.Height}, viewport {scroller.Viewport.Height}).");
Assert.IsTrue(scroller.Viewport.Height <= BoundedChoiceListMaxHeight + 0.5,
$"The choice list viewport must stay bounded (was {scroller.Viewport.Height}).");
Assert.IsTrue(card.Bounds.Height < 600,
$"An expanded option card must not fill the page (height {card.Bounds.Height}).");
Assert.IsTrue(scroller.GetVisualDescendants().OfType<PclListItem>().Count() < 30,
"Long choice lists must remain virtualized inside the bounded card.");
app.Screenshot("install-forge-expanded");

scroller.ScrollToEnd();
await Settle();
var lastChoice = scroller.GetVisualDescendants().OfType<PclListItem>().Single(item => item.Title == "Forge 119");
await Click(app.Window, lastChoice);
await Wait(() => ForgeCard().Bounds.Height <= 45, "Selecting a choice did not collapse the card back to its header height.");
Assert.AreEqual("Forge 119", ((DownloadInstallOptionCardViewModel)ForgeCard().DataContext!).SelectionText);
app.Screenshot("install-forge-selected");
});

private static void SeedCatalog(LauncherViewModel launcher)
{
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var version = typeof(LauncherViewModel).GetField("_downloadInstallMinecraftCatalogVersion", flags)!;
version.SetValue(launcher, (int)version.GetValue(launcher)! + 1);
Set("_downloadInstallMinecraftCatalogLoaded", true);
Set("_isDownloadInstallMinecraftCatalogLoading", false);
Set("_downloadInstallMinecraftCatalogChoices", new[]
{
Choice("1.21.119", "release")
});

void Set(string name, object value) => typeof(LauncherViewModel).GetField(name, flags)!.SetValue(launcher, value);
static FrontendInstallChoice Choice(string id, string group) => new(id, id, id, id, FrontendInstallChoiceKind.Minecraft,
Metadata: new JsonObject { ["catalogGroup"] = group });
}

private static void SeedForgeChoices(LauncherViewModel launcher, int count)
{
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var cache = (Dictionary<string, IReadOnlyList<FrontendInstallChoice>>)typeof(LauncherViewModel)
.GetField("_downloadInstallOptionChoices", flags)!.GetValue(launcher)!;
cache["Forge"] = Enumerable.Range(0, count)
.Select(index => new FrontendInstallChoice(
$"forge-{index}",
$"Forge {index}",
$"{index}.0.{index}",
"1.21.119",
FrontendInstallChoiceKind.Forge))
.ToArray();
}
}
Loading
Loading