初始提交:WinUI 3 一键运行轮盘工具

This commit is contained in:
2026-08-30 01:37:56 +08:00
commit 83011e5d29
58 changed files with 4373 additions and 0 deletions
@@ -0,0 +1,127 @@
using System.Diagnostics;
using OneClickRun.Core.Models;
using OneClickRun.Core.Services;
using Xunit;
namespace OneClickRun.Core.Tests;
public class CommandRunnerTests
{
private static string SystemRoot => Environment.GetFolderPath(Environment.SpecialFolder.Windows);
[Fact]
public void App_BuildsShellExecuteInfo()
{
var item = new CommandItem { ActionType = ActionType.App, Target = SystemRoot + "\\explorer.exe", Arguments = "-n" };
var psi = CommandRunner.BuildStartInfo(item);
Assert.True(psi.UseShellExecute);
Assert.Equal("-n", psi.Arguments);
Assert.False(string.IsNullOrEmpty(psi.WorkingDirectory));
}
[Fact]
public void App_Admin_SetsVerb()
{
var item = new CommandItem { ActionType = ActionType.App, Target = SystemRoot + "\\explorer.exe", RunAsAdmin = true };
var psi = CommandRunner.BuildStartInfo(item);
Assert.Equal("runas", psi.Verb);
}
[Fact]
public void Folder_UsesExplorer()
{
var item = new CommandItem { ActionType = ActionType.Folder, Target = "C:\\Some Folder" };
var psi = CommandRunner.BuildStartInfo(item);
Assert.Equal("explorer.exe", psi.FileName);
Assert.Contains("\"C:\\Some Folder\"", psi.Arguments);
Assert.False(psi.UseShellExecute);
}
[Theory]
[InlineData("www.bing.com", "https://www.bing.com")]
[InlineData("bing.com", "https://bing.com")]
[InlineData("https://example.com/x", "https://example.com/x")]
[InlineData("ms-settings:", "ms-settings:")]
public void Url_Normalization(string input, string expected)
{
Assert.Equal(expected, CommandRunner.NormalizeUrl(input));
}
[Fact]
public void Script_BuildsPowerShellArgs()
{
var item = new CommandItem { ActionType = ActionType.Script, Target = "C:\\Scripts\\demo.ps1" };
var psi = CommandRunner.BuildStartInfo(item);
Assert.Equal("powershell.exe", psi.FileName);
Assert.Contains("-NoProfile", psi.Arguments);
Assert.Contains("-ExecutionPolicy Bypass", psi.Arguments);
Assert.Contains("-File \"C:\\Scripts\\demo.ps1\"", psi.Arguments);
Assert.False(psi.UseShellExecute);
Assert.False(psi.CreateNoWindow);
}
[Fact]
public void Script_Hidden_SetsNoWindow()
{
var item = new CommandItem { ActionType = ActionType.Script, Target = "C:\\a.ps1", HideWindow = true };
var psi = CommandRunner.BuildStartInfo(item);
Assert.True(psi.CreateNoWindow);
Assert.Equal(ProcessWindowStyle.Hidden, psi.WindowStyle);
}
[Fact]
public void Script_Admin_SetsVerb()
{
var item = new CommandItem { ActionType = ActionType.Script, Target = "C:\\a.ps1", RunAsAdmin = true };
var psi = CommandRunner.BuildStartInfo(item);
Assert.True(psi.UseShellExecute);
Assert.Equal("runas", psi.Verb);
}
[Theory]
[InlineData(ActionType.App)]
[InlineData(ActionType.Folder)]
[InlineData(ActionType.Script)]
[InlineData(ActionType.Url)]
public void Validate_MissingTarget_ReturnsError(ActionType type)
{
var item = new CommandItem { Name = "x", ActionType = type, Target = "" };
Assert.NotNull(CommandRunner.Validate(item));
}
[Fact]
public void Validate_ExistingApp_ReturnsNull()
{
var item = new CommandItem { Name = "x", ActionType = ActionType.App, Target = SystemRoot + "\\notepad.exe" };
Assert.Null(CommandRunner.Validate(item));
}
[Fact]
public void Validate_InvalidUrl_ReturnsError()
{
var item = new CommandItem { Name = "x", ActionType = ActionType.Url, Target = "::not a url::" };
Assert.NotNull(CommandRunner.Validate(item));
}
[Theory]
[InlineData(@"C:\Scripts\run.cmd")]
[InlineData(@"C:\Scripts\run.bat")]
public void Script_Batch_BuildsCmdArgs(string target)
{
var item = new CommandItem { ActionType = ActionType.Script, Target = target };
var psi = CommandRunner.BuildStartInfo(item);
Assert.Equal("cmd.exe", psi.FileName);
Assert.Equal("/c \"\"" + target + "\"\"", psi.Arguments);
Assert.False(psi.UseShellExecute);
}
[Fact]
public void Script_Batch_Admin_SetsVerb()
{
var item = new CommandItem { ActionType = ActionType.Script, Target = "C:\run.bat", RunAsAdmin = true };
var psi = CommandRunner.BuildStartInfo(item);
Assert.Equal("cmd.exe", psi.FileName);
Assert.True(psi.UseShellExecute);
Assert.Equal("runas", psi.Verb);
}
}
@@ -0,0 +1,56 @@
using OneClickRun.Core.Logic;
using Xunit;
namespace OneClickRun.Core.Tests;
public class DoubleClickStateTests
{
private static readonly TimeSpan Window = TimeSpan.FromMilliseconds(500);
private static readonly DateTimeOffset T0 = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
[Fact]
public void FirstClick_IsPending()
{
var state = new DoubleClickState();
Assert.Equal(DoubleClickOutcome.Pending, state.OnClick(2, T0, Window));
Assert.Equal(2, state.PendingIndex);
}
[Fact]
public void SecondClick_SameSector_WithinWindow_Executes()
{
var state = new DoubleClickState();
state.OnClick(2, T0, Window);
Assert.Equal(DoubleClickOutcome.Execute, state.OnClick(2, T0.AddMilliseconds(400), Window));
Assert.Equal(-1, state.PendingIndex);
}
[Fact]
public void SecondClick_AfterWindow_RestartsPending()
{
var state = new DoubleClickState();
state.OnClick(2, T0, Window);
Assert.Equal(DoubleClickOutcome.Pending, state.OnClick(2, T0.AddMilliseconds(600), Window));
Assert.Equal(2, state.PendingIndex);
}
[Fact]
public void SecondClick_DifferentSector_MovesPending()
{
var state = new DoubleClickState();
state.OnClick(2, T0, Window);
Assert.Equal(DoubleClickOutcome.Pending, state.OnClick(3, T0.AddMilliseconds(200), Window));
Assert.Equal(3, state.PendingIndex);
Assert.Equal(DoubleClickOutcome.Execute, state.OnClick(3, T0.AddMilliseconds(400), Window));
}
[Fact]
public void Reset_ClearsPending()
{
var state = new DoubleClickState();
state.OnClick(1, T0, Window);
state.Reset();
Assert.Equal(-1, state.PendingIndex);
Assert.False(state.IsPending(T0.AddMilliseconds(1), Window));
}
}
@@ -0,0 +1,53 @@
using OneClickRun.Core.Logic;
using OneClickRun.Core.Models;
using Xunit;
namespace OneClickRun.Core.Tests;
public class HotkeyFormatTests
{
[Fact]
public void Format_Default_IsCtrlAltQ()
{
Assert.Equal("Ctrl+Alt+Q", HotkeyFormat.Format(HotkeyDefinition.Default));
}
[Theory]
[InlineData("Ctrl+Alt+Q", HotkeyModifiers.Control | HotkeyModifiers.Alt, 0x51)]
[InlineData("Ctrl+Shift+F1", HotkeyModifiers.Control | HotkeyModifiers.Shift, 0x70)]
[InlineData("Win+Space", HotkeyModifiers.Win, 0x20)]
[InlineData("Alt+F4", HotkeyModifiers.Alt, 0x73)]
public void TryParse_ValidCombos(string text, HotkeyModifiers expectedMods, int expectedKey)
{
Assert.True(HotkeyFormat.TryParse(text, out var def, out var error), error);
Assert.Equal(expectedMods, def.Modifiers);
Assert.Equal(expectedKey, def.Key);
}
[Theory]
[InlineData("Q")]
[InlineData("Shift+Q")]
[InlineData("Ctrl+")]
[InlineData("Ctrl+Alt+Banana")]
[InlineData("Ctrl+Alt+A+B")]
[InlineData("")]
public void TryParse_InvalidCombos_ReturnsFalse(string text)
{
Assert.False(HotkeyFormat.TryParse(text, out _, out _));
}
[Fact]
public void RoundTrip_ParseThenFormat()
{
Assert.True(HotkeyFormat.TryParse("ctrl+alt+q", out var def, out _));
Assert.Equal("Ctrl+Alt+Q", HotkeyFormat.Format(def));
}
[Fact]
public void GetKeyName_KnownKeys()
{
Assert.Equal("F24", HotkeyFormat.GetKeyName(0x87));
Assert.Equal("A", HotkeyFormat.GetKeyName(0x41));
Assert.Equal("Space", HotkeyFormat.GetKeyName(0x20));
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>OneClickRun.Core.Tests</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OneClickRun.Core\OneClickRun.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,79 @@
using OneClickRun.Core.Models;
using OneClickRun.Core.Services;
using Xunit;
namespace OneClickRun.Core.Tests;
public sealed class SettingsStoreTests : IDisposable
{
private readonly string _dir = Path.Combine(Path.GetTempPath(), "ocr-settings-test-" + Guid.NewGuid().ToString("N"));
private SettingsStore Store => new(_dir);
public void Dispose()
{
try { Directory.Delete(_dir, recursive: true); } catch { /* 忽略 */ }
}
[Fact]
public void Load_MissingFile_ReturnsDefaults()
{
var settings = Store.Load();
Assert.True(settings.WheelEnabled);
Assert.Equal(5, settings.Commands.Count);
Assert.Equal(400, settings.LongPressDelayMs);
}
[Fact]
public void RoundTrip_SaveThenLoad()
{
var settings = new AppSettings
{
StartWithWindows = true,
WheelEnabled = false,
SummonMode = SummonMode.Hold,
SelectionMode = SelectionMode.Swipe,
ThemeMode = ThemeMode.Dark,
LongPressDelayMs = 700,
};
settings.Commands.Add(new CommandItem { Name = "测试", ActionType = ActionType.Url, Target = "https://example.com", ColorIndex = 5 });
Store.Save(settings);
var loaded = Store.Load();
Assert.True(loaded.StartWithWindows);
Assert.False(loaded.WheelEnabled);
Assert.Equal(SummonMode.Hold, loaded.SummonMode);
Assert.Equal(SelectionMode.Swipe, loaded.SelectionMode);
Assert.Equal(ThemeMode.Dark, loaded.ThemeMode);
Assert.Equal(700, loaded.LongPressDelayMs);
var cmd = Assert.Single(loaded.Commands);
Assert.Equal("测试", cmd.Name);
Assert.Equal(5, cmd.ColorIndex);
}
[Fact]
public void Load_CorruptFile_BacksUpAndReturnsDefaults()
{
Directory.CreateDirectory(_dir);
File.WriteAllText(Path.Combine(_dir, "settings.json"), "这不是 JSON {{{");
var settings = Store.Load();
Assert.True(settings.WheelEnabled);
Assert.NotEmpty(Directory.GetFiles(_dir, "settings.json.corrupt-*"));
}
[Fact]
public void Load_InvalidValues_AreNormalized()
{
Directory.CreateDirectory(_dir);
File.WriteAllText(Path.Combine(_dir, "settings.json"),
"{\"SchemaVersion\":1,\"LongPressDelayMs\":99999,\"Hotkey\":null}");
var settings = Store.Load();
Assert.Equal(400, settings.LongPressDelayMs);
Assert.Equal("Ctrl+Alt+Q", Logic.HotkeyFormat.Format(settings.Hotkey));
}
}
@@ -0,0 +1,81 @@
using OneClickRun.Core.Logic;
using Xunit;
namespace OneClickRun.Core.Tests;
public class WheelMathTests
{
[Theory]
[InlineData(0, 0)]
[InlineData(44.9, 0)]
[InlineData(45, 1)]
[InlineData(90, 2)]
[InlineData(359.9, 7)]
[InlineData(-45, 7)]
[InlineData(360, 0)]
public void GetSectorIndex_MapsAngles(double angle, int expected)
{
Assert.Equal(expected, WheelMath.GetSectorIndex(8, angle));
}
[Fact]
public void GetSectorIndex_SingleSector_AlwaysZero()
{
Assert.Equal(0, WheelMath.GetSectorIndex(1, 123.4));
}
[Fact]
public void GetSectorIndex_NoSectors_ReturnsMinusOne()
{
Assert.Equal(-1, WheelMath.GetSectorIndex(0, 10));
}
[Theory]
[InlineData(0, -1, 0)]
[InlineData(1, 0, 90)]
[InlineData(0, 1, 180)]
[InlineData(-1, 0, 270)]
public void AngleFromVector_CardinalDirections(double dx, double dy, double expected)
{
Assert.Equal(expected, WheelMath.AngleFromVector(dx, dy), 3);
}
[Fact]
public void PolarPoint_Top()
{
var (x, y) = WheelMath.PolarPoint(100, 100, 50, 0);
Assert.Equal(100, x, 3);
Assert.Equal(50, y, 3);
}
[Fact]
public void PolarPoint_Right()
{
var (x, y) = WheelMath.PolarPoint(100, 100, 50, 90);
Assert.Equal(150, x, 3);
Assert.Equal(100, y, 3);
}
[Fact]
public void TryGetSwipeIndex_DeadZone_ReturnsFalse()
{
Assert.False(WheelMath.TryGetSwipeIndex(8, 5, 0, 10, out _));
}
[Fact]
public void TryGetSwipeIndex_NoCommands_ReturnsFalse()
{
Assert.False(WheelMath.TryGetSwipeIndex(0, 100, 0, 10, out _));
}
[Theory]
[InlineData(0, -100, 0)]
[InlineData(100, 0, 2)]
[InlineData(0, 100, 4)]
[InlineData(-100, 0, 6)]
public void TryGetSwipeIndex_Directions(double dx, double dy, int expected)
{
Assert.True(WheelMath.TryGetSwipeIndex(8, dx, dy, 10, out int index));
Assert.Equal(expected, index);
}
}