Files
LanMountainDesktop/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs
lincube 9b7a35ddd3 refactor: 新建 Platform 平台差异层,主工程 P/Invoke 全部迁出
- 新增 Platform.Abstractions(接口+NoOp+PlatformLog 日志桥)、
  Platform.Windows、Platform.MacOS、Platform.Android 四个项目
- 迁移电源管理、原生对话框、桌面层嵌入、窗口置底/区域穿透、
  DWM 互操作、图标服务、包标识查询等全部 Windows P/Invoke 实现
- 主工程保留静态工厂门面,调用点不变;DllImport 扫描为零
- 更新 WindowPassthroughServiceTests 指向新位置(20/20 通过)
2026-07-26 22:14:46 +09:00

265 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
{
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "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));
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "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()
{
var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "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)}'.");
}
}