初始提交:WinUI 3 一键运行轮盘工具
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user