Files
LanMountainDesktop/tests/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs

265 lines
9.6 KiB
C#
Raw Permalink Normal View History

using System.Reflection;
using Avalonia;
using LanMountainDesktop.Platform.Abstractions;
using LanMountainDesktop.Platform.Windows;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class WindowPassthroughServiceTests
{
private const uint WsChild = 0x40000000U;
private const uint WsPopup = 0x80000000U;
private const uint WsCaption = 0x00C00000U;
private const uint WsThickFrame = 0x00040000U;
private const uint WsMinimizeBox = 0x00020000U;
private const uint WsMaximizeBox = 0x00010000U;
private const uint WsSysMenu = 0x00080000U;
private const uint WsVisible = 0x10000000U;
private const uint WsExToolWindow = 0x00000080U;
private const uint WsExAppWindow = 0x00040000U;
private const uint WsExNoActivate = 0x08000000U;
private const uint WsExNoRedirectionBitmap = 0x00200000U;
private const uint WsExTopmost = 0x00000008U;
[Fact]
public void DesktopChildStylePolicy_PreservesAvaloniaCompositionBits()
{
var style = WsPopup | WsCaption | WsThickFrame | WsMinimizeBox |
WsMaximizeBox | WsSysMenu | WsVisible;
var exStyle = WsExNoRedirectionBitmap | WsExTopmost | WsExAppWindow;
var result = Invoke<(uint Style, uint ExStyle)>(
"CreateDesktopChildStyles",
style,
exStyle);
Assert.NotEqual(0U, result.Style & WsChild);
Assert.NotEqual(0U, result.Style & WsVisible);
Assert.Equal(0U, result.Style & WsPopup);
Assert.Equal(0U, result.Style & WsCaption);
Assert.Equal(0U, result.Style & WsThickFrame);
Assert.Equal(0U, result.Style & WsMinimizeBox);
Assert.Equal(0U, result.Style & WsMaximizeBox);
Assert.Equal(0U, result.Style & WsSysMenu);
Assert.NotEqual(0U, result.ExStyle & WsExNoRedirectionBitmap);
Assert.NotEqual(0U, result.ExStyle & WsExTopmost);
Assert.NotEqual(0U, result.ExStyle & WsExToolWindow);
Assert.NotEqual(0U, result.ExStyle & WsExNoActivate);
Assert.Equal(0U, result.ExStyle & WsExAppWindow);
}
[Fact]
public void NativeIntegration_UsesAvaloniaCallbacksWithoutManualWndProcSubclassing()
{
2026-08-11 19:41:54 +08:00
var source = ReadRepositoryFile("platform", "LanMountainDesktop.Platform", "WindowsWindowPassthroughServices.cs");
Assert.Contains("Win32Properties.AddWindowStylesCallback", source);
Assert.Contains("Win32Properties.AddWndProcHookCallback", source);
Assert.Contains("Win32Properties.RemoveWindowStylesCallback", source);
Assert.Contains("Win32Properties.RemoveWndProcHookCallback", source);
Assert.DoesNotContain("WS_EX_LAYERED", source);
Assert.DoesNotContain("GWLP_WNDPROC", source);
Assert.DoesNotContain("_originalWndProcs", source);
Assert.Contains("SWP_FRAMECHANGED", source);
Assert.Contains("DwmSetWindowAttribute", source);
Assert.Contains("WindowCornerPreference.DoNotRound", source);
Assert.Contains("NeedsNativeRepair", source);
Assert.Contains("retrying incomplete native rollback", source);
Assert.Contains("if (!restored)", source);
Assert.Contains("SWP_HIDEWINDOW", source);
}
[Theory]
[InlineData(96d, 200d, 120d, 200d, 120d)]
[InlineData(120d, 250d, 150d, 200d, 120d)]
[InlineData(144d, 300d, 180d, 200d, 120d)]
[InlineData(192d, 400d, 240d, 200d, 120d)]
public void HitTestCoordinates_ScalePhysicalPixelsAcrossSupportedDpiRanges(
double dpi,
double physicalX,
double physicalY,
double expectedX,
double expectedY)
{
var result = Invoke<Point>(
"ConvertPhysicalClientPointToDip",
new Point(physicalX, physicalY),
dpi / 96d);
Assert.Equal(new Point(expectedX, expectedY), result);
}
[Fact]
public void HitTesting_ExcludesRoundedTransparentCorners()
{
var region = new WindowInteractiveRegion(new Rect(0, 0, 100, 80), 20);
Assert.False(IsInside(region, new Point(1, 1)));
Assert.False(IsInside(region, new Point(99, 1)));
Assert.False(IsInside(region, new Point(1, 79)));
Assert.False(IsInside(region, new Point(99, 79)));
Assert.True(IsInside(region, new Point(50, 1)));
Assert.True(IsInside(region, new Point(1, 40)));
Assert.True(IsInside(region, new Point(50, 40)));
}
[Fact]
public void HitTesting_SupportsNegativeDesktopCoordinatesAndRectangularEditRegions()
{
var rounded = new WindowInteractiveRegion(new Rect(-100, -50, 100, 80), 20);
var editRegion = new WindowInteractiveRegion(new Rect(-100, -50, 100, 80), 0);
Assert.False(IsInside(rounded, new Point(-99, -49)));
Assert.True(IsInside(rounded, new Point(-50, -49)));
Assert.True(IsInside(rounded, new Point(-50, -10)));
Assert.True(IsInside(editRegion, new Point(-99, -49)));
Assert.False(IsInside(editRegion, new Point(1, -10)));
}
[Fact]
public void HitTesting_UsesInverseRegionTransformForScaledAndTranslatedRoots()
{
var clientToLocal = new Matrix(
0.5, 0,
0, 0.5,
-5, -10);
var region = new WindowInteractiveRegion(
new Rect(0, 0, 100, 80),
20,
clientToLocal);
Assert.False(IsInside(region, new Point(12, 22)));
Assert.True(IsInside(region, new Point(110, 100)));
Assert.False(IsInside(region, new Point(212, 100)));
}
[Fact]
public void HitTesting_UsesLocalRoundedGeometryForRotatedRoots()
{
var clientToLocal = new Matrix(
0, -1,
1, 0,
-50, 200);
var region = new WindowInteractiveRegion(
new Rect(0, 0, 100, 80),
20,
clientToLocal);
Assert.True(IsInside(region, new Point(160, 100)));
Assert.False(IsInside(region, new Point(121, 51)));
Assert.False(IsInside(region, new Point(210, 100)));
}
[Fact]
public void RestoreCoordinatePolicy_UsesParentClientCoordinatesOnlyForOriginalChildWindows()
{
Assert.True(Invoke<bool>("OriginalWindowUsesParentClientCoordinates", WsChild));
Assert.False(Invoke<bool>("OriginalWindowUsesParentClientCoordinates", WsPopup));
Assert.False(Invoke<bool>("OriginalWindowUsesParentClientCoordinates", 0U));
2026-08-11 19:41:54 +08:00
var source = ReadRepositoryFile("platform", "LanMountainDesktop.Platform", "WindowsWindowPassthroughServices.cs");
Assert.Contains("GWLP_HWNDPARENT", source);
Assert.Contains("restore the owner through GWLP_HWNDPARENT", source);
}
[Fact]
public void MonitorPolicy_HonorsPerWindowRepairBackoffAndHostChanges()
{
var now = new DateTime(2026, 7, 14, 12, 0, 0, DateTimeKind.Utc);
Assert.False(Invoke<bool>(
"ShouldAttemptNativeRepair",
false,
now,
now.AddSeconds(30)));
Assert.True(Invoke<bool>(
"ShouldAttemptNativeRepair",
false,
now,
now.AddSeconds(-1)));
Assert.True(Invoke<bool>(
"ShouldAttemptNativeRepair",
true,
now,
now.AddMinutes(1)));
}
[Fact]
public void MonitorPolicy_DoesNotRemountSafeFallbackUntilHostActuallyChanges()
{
var currentHost = new IntPtr(42);
Assert.False(Invoke<bool>(
"ShouldAttemptDesktopAttachment",
false,
IntPtr.Zero,
currentHost,
false));
Assert.True(Invoke<bool>(
"ShouldAttemptDesktopAttachment",
true,
IntPtr.Zero,
currentHost,
false));
Assert.True(Invoke<bool>(
"ShouldAttemptDesktopAttachment",
false,
currentHost,
currentHost,
true));
}
[Fact]
public void HitTestCoordinates_AreResolvedFromTheLiveWindowState()
{
2026-08-11 19:41:54 +08:00
var source = ReadRepositoryFile("platform", "LanMountainDesktop.Platform", "WindowsWindowPassthroughServices.cs");
Assert.Contains("ScreenToClient(hWnd, ref screenPoint)", source);
Assert.Contains("GetDpiForWindow(handle)", source);
Assert.DoesNotContain("_windowScreenOrigins", source);
Assert.DoesNotContain("_windowDpiScales", source);
Assert.DoesNotContain("GetDpiForMonitor", source);
}
private static bool IsInside(WindowInteractiveRegion region, Point point)
{
return Invoke<bool>("IsPointInsideRegion", region, point);
}
private static T Invoke<T>(string methodName, params object[] arguments)
{
// Windows 实现已迁移至 Platform.Windows跨平台重构类型现为 public。
var serviceType = typeof(WindowsWindowBottomMostService);
var method = serviceType.GetMethod(
methodName,
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
Assert.NotNull(method);
return (T)method.Invoke(null, arguments)!;
}
private static string ReadRepositoryFile(params string[] segments)
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
if (File.Exists(candidate))
{
return File.ReadAllText(candidate);
}
if (File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
{
break;
}
directory = directory.Parent;
}
throw new FileNotFoundException($"Could not locate repository file '{Path.Combine(segments)}'.");
}
}