Compare commits

...

16 Commits

Author SHA1 Message Date
Clint Rutkas
ab08e318d8 Fix MathHelper.Modulo for non-positive divisors; improve test coverage
- Add ArgumentOutOfRangeException guard for b<=0 in MathHelper.Modulo
  (previously threw raw DivideByZeroException for b=0, returned wrong
  results for negative divisors)
- Add test for invalid Position enum (only untested code branch)
- Add test for negative Modulo divisor
- Update zero-divisor test to expect ArgumentOutOfRangeException
- Remove redundant AllPositions smoke test (covered by 9 exact-value tests)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-13 17:20:31 -07:00
Clint Rutkas
ce0439f580 Enhance PowerAccent.Core and Peek.Common tests: fix bugs, strengthen assertions
- Fixed Rect.cs: Added Width/Height zero-division guards (was producing Infinity)
- Fixed Size.cs: Removed duplicate condition in division operator
- Strengthened Currency test with specific symbol assertions (euro, dollar, pound)
- Added cache identity check with Assert.AreSame
- Renamed misleading UNC test method (UncFileUri -> StandardUncWithFile)
- Added 10 new tests: Rect zero-Width/Height/AllZero, int.MinValue overflow,
  Modulo zero divisor, TopRight/BottomLeft exact values, null UNC path,
  Point both-zero, Size both-zero
- Added XML doc comments to all 123 test methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 19:09:21 -07:00
Clint Rutkas
5b7d3c0a82 Fix deps.json version conflicts in Peek.Common.UnitTests
Remove Common.SelfContained.props (only 3 of 72 test projects use it) and add
UseWindowsForms to bring in the WindowsDesktop shared framework. This ensures
runtime DLLs (System.CodeDom, Microsoft.VisualBasic, WindowsBase, System.Drawing)
resolve to the same versions as all other projects in the solution.

Without UseWindowsForms, Peek.Common (which uses WinUI, not WPF/WinForms) doesn't
transitively provide the WindowsDesktop framework reference, causing NuGet package
versions to appear in deps.json instead of the shared runtime versions.

Verified: All 487 libraries pass verifyDepsJsonLibraryVersions.ps1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 14:04:38 -07:00
Clint Rutkas
5e12d2476d Merge remote-tracking branch 'origin/copilot/upgrade-microsoft-semantic-kernel-core' into copilot/improve-test-coverage 2026-04-11 15:23:42 -07:00
Clint Rutkas
3b6bf9c0ce Fix deps.json version conflicts for System.Diagnostics.EventLog and System.Threading.Channels
Add PackageReference for System.Diagnostics.EventLog and System.Threading.Channels
to all C# projects via Directory.Build.props, ensuring the NuGet 10.x versions
take precedence over the 9.x versions bundled in the .NET 9 runtime. This resolves
the verifyDepsJsonLibraryVersions CI check failure where different projects would
reference different DLL versions of these libraries.

- Add System.Threading.Channels 10.0.4 to Directory.Packages.props
- Add PackageReference for both packages in Directory.Build.props (C# only)
- Remove ExcludeAssets=runtime from System.Diagnostics.EventLog in 6 projects
  (no longer needed since we want the 10.x NuGet version to override the runtime)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 14:56:16 -07:00
Clint Rutkas
9020b18a9f Upgrade transitive dependencies for SemanticKernel 1.74.0
Upgrade Microsoft.Extensions.*, OpenAI, System.Text.Json, and other
transitive dependencies to versions compatible with SemanticKernel 1.74.0:

- Microsoft.Extensions.AI: 9.9.1 -> 10.4.1
- Microsoft.Extensions.AI.OpenAI: 9.9.1-preview -> 10.4.1
- Microsoft.Extensions.Caching.Abstractions: 9.0.10 -> 10.0.5
- Microsoft.Extensions.Caching.Memory: 9.0.10 -> 10.0.5
- Microsoft.Extensions.DependencyInjection: 9.0.10 -> 10.0.5
- Microsoft.Extensions.Logging: 9.0.10 -> 10.0.5
- Microsoft.Extensions.Logging.Abstractions: 9.0.10 -> 10.0.5
- Microsoft.Extensions.Hosting: 9.0.10 -> 10.0.5
- Microsoft.Extensions.Hosting.WindowsServices: 9.0.10 -> 10.0.5
- Microsoft.Bcl.AsyncInterfaces: 9.0.10 -> 10.0.5
- Newtonsoft.Json: 13.0.3 -> 13.0.4
- OpenAI: 2.5.0 -> 2.9.1
- System.ClientModel: 1.7.0 -> 1.9.0
- System.Diagnostics.EventLog: 9.0.10 -> 10.0.5
- System.Numerics.Tensors: 9.0.11 -> 10.0.4
- System.ServiceProcess.ServiceController: 9.0.10 -> 10.0.5
- System.Text.Json: 9.0.10 -> 10.0.5

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 13:04:10 -07:00
Clint Rutkas
f59f67cb08 Merge remote-tracking branch 'origin/main' into copilot/improve-test-coverage 2026-04-11 12:24:44 -07:00
copilot-swe-agent[bot]
890ea40f8a Upgrade Microsoft.SemanticKernel packages from 1.66.0 to 1.74.0
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/39021150-d623-45a0-98e1-6eb8a65d44c0

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-11 04:10:23 +00:00
copilot-swe-agent[bot]
70dd9db67a Fix deps.json version mismatch: add SelfContained props to test projects
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/a7787ea5-42eb-4e09-96c8-c295385faa9c

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-05 23:14:55 +00:00
copilot-swe-agent[bot]
fb7c945a2c Fix SA1508 and CS0246: remove blank lines before closing braces, add missing using System
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/a6ae0e3d-68ed-4a4f-8b91-e039e2f05689

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-05 16:37:15 +00:00
copilot-swe-agent[bot]
c72580c8f2 Fix build error: add missing using System.Linq to LanguagesTests.cs
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/0b83d8fc-328d-47ec-a746-685956604afe

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-04 05:08:11 +00:00
copilot-swe-agent[bot]
ce6debf68b Add traies and udit to spell-check allow list
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/1daa557f-51f2-454e-a7f9-aacc3912d36f

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-03 01:41:54 +00:00
copilot-swe-agent[bot]
1eca1713e1 Remove all #region/#endregion directives to fix SA1124 errors
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/00075017-9397-4cb6-ad53-1afda756d401

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-01 20:20:30 +00:00
copilot-swe-agent[bot]
1254cba088 Add test projects to PowerToys.slnx so CI discovers and runs them
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/9ffea101-795b-4b8c-817a-ab5af7004f72

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-01 04:40:47 +00:00
copilot-swe-agent[bot]
b5373cbb2b Update spell-check: add Ene to allow list, remove unused entries from expect list
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/717795b3-9c5f-484e-9b9c-63de37852f81

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-03-31 20:39:18 +00:00
copilot-swe-agent[bot]
b82e6c508d Add unit tests for PowerAccent.Core and Peek.Common helpers
Add PowerAccent.Core.UnitTests project with tests for:
- Point, Size, Rect struct constructors and division operators
- Calculation position logic with boundary conditions and DPI
- Languages character mapping for multiple languages

Add Peek.Common.UnitTests project with tests for:
- MathHelper.Modulo with positive/negative values
- MathHelper.NumberOfDigits with edge cases
- PathHelper.IsUncPath with various path formats

Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/643cb57b-c352-45a6-83d0-d6b09011290b

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-03-31 19:10:40 +00:00
24 changed files with 1816 additions and 52 deletions

View File

@@ -328,6 +328,9 @@ MRUCMPPROC
MRUINFO
REGSTR
# Quick Accent
Ene
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
MEMORYSTATUSEX
@@ -389,5 +392,9 @@ nostdin
engtype
Nonpaged
# Spell-check fragments
traies
udit
# XAML
Untargeted

View File

@@ -791,7 +791,6 @@ lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
@@ -833,7 +832,6 @@ lstrlen
LTEXT
LTRREADING
luid
LUMA
lusrmgr
LVal
LWA
@@ -852,7 +850,6 @@ MAPPEDTOSAMEKEY
MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXDWORD
MAXSHORTCUTSIZE
maxversiontested
MBM
@@ -917,7 +914,6 @@ MOUSEINPUT
MOVESIZEEND
MOVESIZESTART
MRM
MRT
mru
msc
mscorlib
@@ -2303,7 +2299,6 @@ virama
vnd
vredraw
VSpeed
VSync
WASDK
WCRAPI
wft

View File

@@ -85,6 +85,14 @@
<ForceImportBeforeCppProps>$(RepoRoot)Cpp.Build.props</ForceImportBeforeCppProps>
</PropertyGroup>
<!-- Force all C# projects to reference these packages from NuGet so their 10.x versions
take precedence over the 9.x versions bundled in the .NET 9 runtime. Without this,
projects that don't transitively depend on these packages would get the runtime 9.x
version, causing deps.json version conflicts with projects that do. -->
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<PackageReference Include="System.Diagnostics.EventLog" />
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
<PackageReference Include="StyleCop.Analyzers">

View File

@@ -42,26 +42,26 @@
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.250303.1" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.4.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.4.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="0.3.0" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.74.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.74.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.74.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.74.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.74.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.74.0-alpha" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
@@ -89,11 +89,11 @@
<PackageVersion Include="MSTest" Version="$(MSTestVersion)" />
<PackageVersion Include="MSTest.TestFramework" Version="$(MSTestVersion)" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.5.0" />
<PackageVersion Include="OpenAI" Version="2.9.1" />
<PackageVersion Include="Polly.Core" Version="8.6.5" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
@@ -113,23 +113,27 @@
<PackageVersion Include="System.Data.OleDb" Version="9.0.10" />
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
<!-- Package System.Diagnostics.EventLog force-pinned to 10.x to match other Microsoft.Extensions packages;
referenced by all C# projects (via Directory.Build.props) to override the 9.x version from the .NET runtime. -->
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.5" />
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
<PackageVersion Include="System.ClientModel" Version="1.9.0" />
<PackageVersion Include="System.Drawing.Common" Version="9.0.10" />
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="9.0.10" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
<PackageVersion Include="System.Numerics.Tensors" Version="10.0.4" />
<!-- Including System.Threading.Channels to force version across all projects;
referenced by all C# projects (via Directory.Build.props) to override the 9.x version from the .NET runtime. -->
<PackageVersion Include="System.Threading.Channels" Version="10.0.4" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="10.0.5" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />

View File

@@ -791,6 +791,10 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/peek/peek/peek.vcxproj" Id="a1425b53-3d61-4679-8623-e64a0d3d0a48" />
<Project Path="src/modules/peek/Peek.Common.UnitTests/Peek.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/PowerAccent/">
<Project Path="src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj">
@@ -803,6 +807,10 @@
</Project>
<Project Path="src/modules/poweraccent/PowerAccentKeyboardService/PowerAccentKeyboardService.vcxproj" Id="c97d9a5d-206c-454e-997e-009e227d7f02" />
<Project Path="src/modules/poweraccent/PowerAccentModuleInterface/PowerAccentModuleInterface.vcxproj" Id="34a354c5-23c7-4343-916c-c52daf4fc39d" />
<Project Path="src/modules/poweraccent/PowerAccent.Core.UnitTests/PowerAccent.Core.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/PowerOCR/">
<Project Path="src/modules/PowerOCR/PowerOCR/PowerOCR.csproj">

View File

@@ -17,10 +17,7 @@
<PackageReference Include="MSTest" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
<PackageReference Include="System.Diagnostics.EventLog">
<!-- This package is a dependency of Microsoft.Extensions.Logging.EventLog, but we need to set it here so we can exclude the assets, so it doesn't conflict with the 8.0.1 dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
<PackageReference Include="CommunityToolkit.Common" />
</ItemGroup>

View File

@@ -20,9 +20,7 @@
<PackageReference Include="System.Configuration.ConfigurationManager">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
<PackageReference Include="System.Diagnostics.PerformanceCounter">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>

View File

@@ -42,9 +42,7 @@
<PackageReference Include="System.Configuration.ConfigurationManager">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
<PackageReference Include="System.Diagnostics.PerformanceCounter">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>

View File

@@ -20,9 +20,7 @@
<ItemGroup>
<PackageReference Include="System.ServiceProcess.ServiceController" />
<PackageReference Include="System.Diagnostics.EventLog">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
</ItemGroup>
<ItemGroup>

View File

@@ -35,9 +35,7 @@
<PackageReference Include="System.Configuration.ConfigurationManager">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
<PackageReference Include="System.Diagnostics.PerformanceCounter">
<ExcludeAssets>runtime</ExcludeAssets> <!-- Should already be present on .net sdk runtime, so we avoid the conflicting runtime version from nuget -->
</PackageReference>

View File

@@ -0,0 +1,283 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Peek.Common.Helpers;
namespace Peek.Common.UnitTests
{
[TestClass]
public class MathHelperTests
{
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies standard modulo for positive numbers (7 mod 3 = 1)
/// Why: Baseline correctness — ensures the positive path matches C# % operator
/// </summary>
[TestMethod]
public void Modulo_PositiveNumbers_ShouldReturnStandardModulo()
{
Assert.AreEqual(1, MathHelper.Modulo(7, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies that 0 mod n = 0
/// Why: Zero dividend is a common boundary — must not throw or return non-zero
/// </summary>
[TestMethod]
public void Modulo_ZeroDividend_ShouldReturnZero()
{
Assert.AreEqual(0, MathHelper.Modulo(0, 5));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies that exact division yields zero remainder
/// Why: Guards against off-by-one in the modulo formula
/// </summary>
[TestMethod]
public void Modulo_ExactDivision_ShouldReturnZero()
{
Assert.AreEqual(0, MathHelper.Modulo(6, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int) — negative dividend path
/// What: Verifies that -1 mod 3 = 2 (mathematical modulo, not C# remainder)
/// Why: C# % returns -1 for negative dividends; Modulo wraps to positive range
/// </summary>
[TestMethod]
public void Modulo_NegativeDividend_ShouldReturnPositiveResult()
{
Assert.AreEqual(2, MathHelper.Modulo(-1, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int) — negative dividend with larger magnitude
/// What: Verifies -7 mod 3 = 2 (wraps correctly for larger negative values)
/// Why: Tests the full formula: ((-7 % 3) + 3) % 3 = (-1 + 3) % 3 = 2
/// </summary>
[TestMethod]
public void Modulo_NegativeDividend_LargerMagnitude_ShouldWrapCorrectly()
{
Assert.AreEqual(2, MathHelper.Modulo(-7, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies -6 mod 3 = 0 (exact negative multiple)
/// Why: Edge case where negative dividend is an exact multiple of divisor
/// </summary>
[TestMethod]
public void Modulo_NegativeDividend_ExactMultiple_ShouldReturnZero()
{
Assert.AreEqual(0, MathHelper.Modulo(-6, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies n mod 1 = 0 for positive n
/// Why: Divisor of 1 always yields 0 — guards against divide-by-zero edge case
/// </summary>
[TestMethod]
public void Modulo_PositiveDividend_DivisorOne_ShouldReturnZero()
{
Assert.AreEqual(0, MathHelper.Modulo(5, 1));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies -n mod 1 = 0 for negative n
/// Why: Tests the negative path with divisor=1 (simplest negative case)
/// </summary>
[TestMethod]
public void Modulo_NegativeDividend_DivisorOne_ShouldReturnZero()
{
Assert.AreEqual(0, MathHelper.Modulo(-5, 1));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies modulo works with large numbers (1000001 mod 1000000 = 1)
/// Why: Tests that no integer overflow occurs in the formula for large operands
/// </summary>
[TestMethod]
public void Modulo_LargePositiveNumbers_ShouldWork()
{
Assert.AreEqual(1, MathHelper.Modulo(1000001, 1000000));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int)
/// What: Verifies that dividend less than divisor returns the dividend itself
/// Why: 2 mod 5 = 2 — no division happens, just returns the dividend
/// </summary>
[TestMethod]
public void Modulo_DividendLessThanDivisor_ShouldReturnDividend()
{
Assert.AreEqual(2, MathHelper.Modulo(2, 5));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int) — negative dividend
/// What: Verifies -2 mod 3 = 1 (mathematical modulo)
/// Why: Tests another negative wrap case: ((-2 % 3) + 3) % 3 = (-2 + 3) % 3 = 1
/// </summary>
[TestMethod]
public void Modulo_NegativeDividend_MinusTwo_ModThree_ShouldReturnOne()
{
Assert.AreEqual(1, MathHelper.Modulo(-2, 3));
}
/// <summary>
/// Product code: MathHelper.Modulo(int, int) — zero divisor
/// What: Documents that dividing by zero throws DivideByZeroException
/// Why: Edge case — callers must handle zero divisor; the method does not guard against it
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Modulo_ZeroDivisor_ShouldThrow()
{
MathHelper.Modulo(5, 0);
}
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Modulo_NegativeDivisor_ShouldThrow()
{
MathHelper.Modulo(-1, -3);
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int)
/// What: Verifies that 0 has 1 digit
/// Why: Zero is a special case — Math.Abs(0).ToString() = "0" which has length 1
/// </summary>
[TestMethod]
public void NumberOfDigits_Zero_ShouldReturnOne()
{
Assert.AreEqual(1, MathHelper.NumberOfDigits(0));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int)
/// What: Verifies single-digit number returns 1
/// Why: Baseline case for the simplest positive input
/// </summary>
[TestMethod]
public void NumberOfDigits_SingleDigit_ShouldReturnOne()
{
Assert.AreEqual(1, MathHelper.NumberOfDigits(5));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int)
/// What: Verifies two-digit number returns 2
/// Why: Tests the transition from single to multi-digit
/// </summary>
[TestMethod]
public void NumberOfDigits_TwoDigits_ShouldReturnTwo()
{
Assert.AreEqual(2, MathHelper.NumberOfDigits(42));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int)
/// What: Verifies three-digit number (100) returns 3
/// Why: Tests exact power of 10 boundary (100 vs 99)
/// </summary>
[TestMethod]
public void NumberOfDigits_ThreeDigits_ShouldReturnThree()
{
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — negative number
/// What: Verifies that sign is ignored via Math.Abs
/// Why: Negative sign is not a digit — only magnitude matters
/// </summary>
[TestMethod]
public void NumberOfDigits_NegativeNumber_ShouldIgnoreSign()
{
Assert.AreEqual(3, MathHelper.NumberOfDigits(-123));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — int.MaxValue
/// What: Verifies int.MaxValue (2147483647) returns 10 digits
/// Why: Upper boundary of int range — ensures no overflow in Math.Abs path
/// </summary>
[TestMethod]
public void NumberOfDigits_MaxValue_ShouldReturnTenDigits()
{
Assert.AreEqual(10, MathHelper.NumberOfDigits(int.MaxValue));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — near int.MinValue
/// What: Verifies int.MinValue + 1 (-2147483647) returns 10 digits
/// Why: int.MinValue itself overflows Math.Abs — this tests the safe boundary
/// </summary>
[TestMethod]
public void NumberOfDigits_MinValue_ShouldHandleAbsoluteValue()
{
Assert.AreEqual(10, MathHelper.NumberOfDigits(int.MinValue + 1));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — int.MinValue
/// What: Documents that int.MinValue causes OverflowException due to Math.Abs
/// Why: Edge case — Math.Abs(-2147483648) has no positive int representation
/// </summary>
[TestMethod]
[ExpectedException(typeof(OverflowException))]
public void NumberOfDigits_MinValue_ShouldThrowOverflow()
{
MathHelper.NumberOfDigits(int.MinValue);
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — powers of 10
/// What: Verifies digit count at each power-of-10 boundary (1, 10, 100, 1000, 10000)
/// Why: Powers of 10 are the exact transition points — off-by-one bugs surface here
/// </summary>
[TestMethod]
public void NumberOfDigits_PowerOfTen_ShouldReturnCorrectCount()
{
Assert.AreEqual(1, MathHelper.NumberOfDigits(1));
Assert.AreEqual(2, MathHelper.NumberOfDigits(10));
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
Assert.AreEqual(4, MathHelper.NumberOfDigits(1000));
Assert.AreEqual(5, MathHelper.NumberOfDigits(10000));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — 9→10 boundary
/// What: Verifies that 9 returns 1 digit and 10 returns 2 digits
/// Why: Tests the first single-to-double digit transition
/// </summary>
[TestMethod]
public void NumberOfDigits_BoundaryValues_NineAndTen()
{
Assert.AreEqual(1, MathHelper.NumberOfDigits(9));
Assert.AreEqual(2, MathHelper.NumberOfDigits(10));
}
/// <summary>
/// Product code: MathHelper.NumberOfDigits(int) — 99→100 boundary
/// What: Verifies that 99 returns 2 digits and 100 returns 3 digits
/// Why: Tests the double-to-triple digit transition
/// </summary>
[TestMethod]
public void NumberOfDigits_BoundaryValues_NinetyNineAndHundred()
{
Assert.AreEqual(2, MathHelper.NumberOfDigits(99));
Assert.AreEqual(3, MathHelper.NumberOfDigits(100));
}
}
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Peek.Common.Helpers;
namespace Peek.Common.UnitTests
{
[TestClass]
public class PathHelperTests
{
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a standard UNC path (\\server\share) is recognized
/// Why: Baseline positive case — the most common UNC format
/// </summary>
[TestMethod]
public void IsUncPath_StandardUncPath_ShouldReturnTrue()
{
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a UNC path with nested subfolders and file is recognized
/// Why: Real-world UNC paths include subfolders — must not fail on deeper paths
/// </summary>
[TestMethod]
public void IsUncPath_UncPathWithSubfolder_ShouldReturnTrue()
{
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share\folder\file.txt"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a UNC path with dotted server name (FQDN) is recognized
/// Why: Enterprise environments use FQDN server names (e.g., server.corp.com)
/// </summary>
[TestMethod]
public void IsUncPath_UncPathWithDottedServer_ShouldReturnTrue()
{
Assert.IsTrue(PathHelper.IsUncPath(@"\\server.domain.com\share"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a UNC path with IP address is recognized
/// Why: Some network shares use IP addresses instead of hostnames
/// </summary>
[TestMethod]
public void IsUncPath_UncPathWithIPAddress_ShouldReturnTrue()
{
Assert.IsTrue(PathHelper.IsUncPath(@"\\192.168.1.1\share"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a local drive path (C:\...) is not classified as UNC
/// Why: Drive-letter paths are local — confusing them with UNC would break file access
/// </summary>
[TestMethod]
public void IsUncPath_LocalDrivePath_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(@"C:\Users\test\file.txt"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a bare drive root (D:\) is not classified as UNC
/// Why: Drive roots are local paths — shortest possible local absolute path
/// </summary>
[TestMethod]
public void IsUncPath_LocalRootPath_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(@"D:\"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a relative path is not classified as UNC
/// Why: Relative paths have no server component — Uri.TryCreate fails for them
/// </summary>
[TestMethod]
public void IsUncPath_RelativePath_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(@"folder\file.txt"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies an empty string returns false without throwing
/// Why: Empty input is a common edge case — must not crash
/// </summary>
[TestMethod]
public void IsUncPath_EmptyString_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(string.Empty));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies an HTTP URL is not classified as UNC
/// Why: HTTP URLs are not file paths — Uri.IsUnc must return false for them
/// </summary>
[TestMethod]
public void IsUncPath_HttpUrl_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath("http://example.com/path"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a file:/// URI (local file) is not classified as UNC
/// Why: file:///C:/... is a local file URI, not a network UNC path
/// </summary>
[TestMethod]
public void IsUncPath_FileUri_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath("file:///C:/Users/test/file.txt"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a standard UNC path with a file component (\\server\share\file.txt)
/// Why: Tests UNC with a trailing file name — distinct from share-only paths
/// </summary>
[TestMethod]
public void IsUncPath_StandardUncWithFile_ShouldReturnTrue()
{
Assert.IsTrue(PathHelper.IsUncPath(@"\\server\share\file.txt"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies a single-backslash prefix is not classified as UNC
/// Why: UNC requires exactly two leading backslashes — one backslash is not UNC
/// </summary>
[TestMethod]
public void IsUncPath_SingleBackslash_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(@"\server\share"));
}
/// <summary>
/// Product code: PathHelper.IsUncPath(string)
/// What: Verifies null input returns false without throwing
/// Why: Null is a common edge case — Uri.TryCreate handles null gracefully
/// </summary>
[TestMethod]
public void IsUncPath_NullInput_ShouldReturnFalse()
{
Assert.IsFalse(PathHelper.IsUncPath(null));
}
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Peek.Common.Tests\</OutputPath>
<RootNamespace>Peek.Common.UnitTests</RootNamespace>
<AssemblyName>PowerToys.Peek.Common.UnitTests</AssemblyName>
<OutputType>Exe</OutputType>
<!-- Pull in the WindowsDesktop shared framework so that runtime DLLs (System.CodeDom,
Microsoft.VisualBasic, WindowsBase, etc.) resolve to the same version as all other
projects in the solution. Without this, transitive NuGet packages provide older
versions that fail the verifyDepsJsonLibraryVersions CI check. -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,6 +11,11 @@ namespace Peek.Common.Helpers
{
public static int Modulo(int a, int b)
{
if (b <= 0)
{
throw new ArgumentOutOfRangeException(nameof(b), b, "Divisor must be positive.");
}
return a < 0 ? ((a % b) + b) % b : a % b;
}

View File

@@ -0,0 +1,369 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerAccent.Core.Services;
using PowerAccent.Core.Tools;
namespace PowerAccent.Core.UnitTests
{
[TestClass]
public class CalculationTests
{
// Screen representing a standard 1920x1080 monitor at position (0,0)
private static readonly Rect StandardScreen = new Rect(0, 0, 1920, 1080);
// A typical toolbar window size (300x50 in WPF DIPs)
private static readonly Size ToolbarWindow = new Size(300, 50);
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret
/// What: Verifies toolbar is centered horizontally above the caret at screen center
/// Why: The most common placement scenario — regression here affects every user
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_CenterOfScreen_ShouldCenterToolbar()
{
var caret = new Point(960, 540);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// X should be caret.X - window.Width/2 = 960 - 150 = 810
Assert.AreEqual(810.0, result.X);
// Y should be caret.Y - window.Height - 20 = 540 - 50 - 20 = 470
Assert.AreEqual(470.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — left edge clamping
/// What: Verifies toolbar is clamped to screen left when caret is near left edge
/// Why: Without clamping, toolbar would extend off-screen and be unusable
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_NearLeftEdge_ShouldClampToScreenLeft()
{
var caret = new Point(50, 540);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// left = 50 - 150 = -100, which is < screen.X (0), so X should be clamped to 0
Assert.AreEqual(0.0, result.X);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — right edge clamping
/// What: Verifies toolbar is clamped to screen right when caret is near right edge
/// Why: Toolbar extending past screen right would be clipped by the display
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_NearRightEdge_ShouldClampToScreenRight()
{
var caret = new Point(1900, 540);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// left = 1900 - 150 = 1750, left + window.Width = 1750 + 300 = 2050 > 1920
// So X should be clamped to screen.X + screen.Width - window.Width = 1920 - 300 = 1620
Assert.AreEqual(1620.0, result.X);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — top edge flip
/// What: Verifies toolbar is placed below the caret when there is no room above
/// Why: When caret is near top of screen, toolbar must flip below to remain visible
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_NearTopEdge_ShouldPlaceBelow()
{
var caret = new Point(960, 30);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// top = 30 - 50 - 20 = -40, which is < screen.Y (0)
// So Y should be caret.Y + 20 = 50 (placed below caret)
Assert.AreEqual(50.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — normal above placement
/// What: Verifies toolbar is placed above the caret when there is sufficient space
/// Why: Default behavior — toolbar should appear above to avoid occluding text below
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_SufficientSpaceAbove_ShouldPlaceAbove()
{
var caret = new Point(960, 200);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// top = 200 - 50 - 20 = 130, which is >= screen.Y (0)
Assert.AreEqual(130.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — multi-monitor offset
/// What: Verifies toolbar respects non-zero screen origin (second monitor at X=1920)
/// Why: Multi-monitor setups have offset screen coordinates — clamping must use screen.X
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_OffsetScreen_ShouldRespectScreenOrigin()
{
var screen = new Rect(1920, 0, 1920, 1080);
var caret = new Point(1950, 540);
var result = Calculation.GetRawCoordinatesFromCaret(caret, screen, ToolbarWindow);
// left = 1950 - 150 = 1800, which is < screen.X (1920)
// So X should be clamped to 1920
Assert.AreEqual(1920.0, result.X);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromCaret — exact boundary case
/// What: Verifies no clamping occurs when toolbar fits exactly at the boundary
/// Why: Off-by-one errors in boundary checks are common — this catches ≤ vs &lt; bugs
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromCaret_ExactBoundary_ShouldNotClamp()
{
var caret = new Point(150, 100);
var result = Calculation.GetRawCoordinatesFromCaret(caret, StandardScreen, ToolbarWindow);
// left = 150 - 150 = 0, which == screen.X, so no left clamp needed
Assert.AreEqual(0.0, result.X);
// top = 100 - 50 - 20 = 30, which >= 0
Assert.AreEqual(30.0, result.Y);
}
[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void GetRawCoordinatesFromPosition_InvalidPosition_ShouldThrow()
{
Calculation.GetRawCoordinatesFromPosition((Position)999, StandardScreen, ToolbarWindow, 1.0);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Top
/// What: Verifies toolbar is horizontally centered at the top of the screen with offset
/// Why: Exact value test — catches formula regressions in the Top positioning branch
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_TopCenter_ShouldBeCenteredAtTop()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.Top, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - 150 = 810
Assert.AreEqual(810.0, result.X);
// Y: screen.Y + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Bottom
/// What: Verifies toolbar is horizontally centered at the bottom of the screen
/// Why: Bottom placement subtracts window height + offset — easy to get wrong
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_BottomCenter_ShouldBeCenteredAtBottom()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.Bottom, StandardScreen, ToolbarWindow, 1.0);
// X: centered = 810
Assert.AreEqual(810.0, result.X);
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
Assert.AreEqual(1006.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Center
/// What: Verifies toolbar is centered both horizontally and vertically
/// Why: Center is the simplest case — validates the baseline formula for both axes
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_Center_ShouldBeTrulyCentered()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.Center, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - 150 = 810
Assert.AreEqual(810.0, result.X);
// Y: screen.Y + screen.Height/2 - window.Height*dpi/2 = 0 + 540 - 25 = 515
Assert.AreEqual(515.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.TopLeft
/// What: Verifies toolbar is placed at top-left corner with offset margin
/// Why: Corner placements use raw offset — validates the simplest X/Y path
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_TopLeft_ShouldBeAtTopLeftCorner()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.X);
// Y: screen.Y + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.TopRight
/// What: Verifies exact coordinates for top-right corner placement
/// Why: TopRight combines right-aligned X with top Y — validates both formula branches
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_TopRight_ShouldBeAtTopRightCorner()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopRight, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300 + 24) = 1596
Assert.AreEqual(1596.0, result.X);
// Y: screen.Y + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.BottomLeft
/// What: Verifies exact coordinates for bottom-left corner placement
/// Why: BottomLeft combines left-aligned X with bottom Y — validates both formula branches
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_BottomLeft_ShouldBeAtBottomLeftCorner()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomLeft, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.X);
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
Assert.AreEqual(1006.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.BottomRight
/// What: Verifies toolbar is placed at bottom-right corner with offset margin
/// Why: BottomRight uses the most complex formula (subtracts from both edges)
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_BottomRight_ShouldBeAtBottomRightCorner()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomRight, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300 + 24) = 1596
Assert.AreEqual(1596.0, result.X);
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50 + 24) = 1006
Assert.AreEqual(1006.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Left
/// What: Verifies toolbar is placed at left edge, vertically centered
/// Why: Left position uses offset for X and centering for Y — tests the mix
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_Left_ShouldBeAtLeftMiddle()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.Left, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.X);
// Y: centered vertically = 0 + 540 - 25 = 515
Assert.AreEqual(515.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with Position.Right
/// What: Verifies toolbar is placed at right edge, vertically centered
/// Why: Right position subtracts window width from screen edge — tests the right-align formula
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_Right_ShouldBeAtRightMiddle()
{
var result = Calculation.GetRawCoordinatesFromPosition(Position.Right, StandardScreen, ToolbarWindow, 1.0);
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 1920 - 324 = 1596
Assert.AreEqual(1596.0, result.X);
// Y: centered vertically = 515
Assert.AreEqual(515.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=1.5
/// What: Verifies that DPI scaling is applied to window size in centering formula
/// Why: High-DPI monitors are common — incorrect scaling makes toolbar appear off-center
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_WithHighDpi_ShouldScaleWindow()
{
double dpi = 1.5;
var result = Calculation.GetRawCoordinatesFromPosition(Position.Center, StandardScreen, ToolbarWindow, dpi);
// X: screen.X + screen.Width/2 - window.Width*dpi/2 = 0 + 960 - (300*1.5)/2 = 960 - 225 = 735
Assert.AreEqual(735.0, result.X);
// Y: screen.Y + screen.Height/2 - window.Height*dpi/2 = 0 + 540 - (50*1.5)/2 = 540 - 37.5 = 502.5
Assert.AreEqual(502.5, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=2.0, TopLeft
/// What: Verifies that offset is NOT DPI-scaled for corner positions
/// Why: The 24px offset is a fixed margin — DPI scaling only affects window size
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_WithDpi2_TopLeft_ShouldUseOffset()
{
double dpi = 2.0;
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, StandardScreen, ToolbarWindow, dpi);
// X: screen.X + offset = 0 + 24 = 24 (offset is not DPI-scaled)
Assert.AreEqual(24.0, result.X);
// Y: screen.Y + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with DPI=2.0, BottomRight
/// What: Verifies DPI scaling of window size in bottom-right corner formula
/// Why: At 2x DPI, the window occupies 600×100 pixels — offset from edge must account for this
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_WithDpi2_BottomRight_ShouldScaleWindowSize()
{
double dpi = 2.0;
var result = Calculation.GetRawCoordinatesFromPosition(Position.BottomRight, StandardScreen, ToolbarWindow, dpi);
// X: screen.X + screen.Width - (window.Width*dpi + offset) = 0 + 1920 - (300*2 + 24) = 1920 - 624 = 1296
Assert.AreEqual(1296.0, result.X);
// Y: screen.Y + screen.Height - (window.Height*dpi + offset) = 0 + 1080 - (50*2 + 24) = 1080 - 124 = 956
Assert.AreEqual(956.0, result.Y);
}
/// <summary>
/// Product code: Calculation.GetRawCoordinatesFromPosition with offset screen origin
/// What: Verifies screen.X is added as the base for TopLeft on a secondary monitor
/// Why: Without adding screen origin, toolbar appears on the wrong monitor
/// </summary>
[TestMethod]
public void GetRawCoordinatesFromPosition_OffsetScreen_ShouldRespectScreenOrigin()
{
var screen = new Rect(1920, 0, 1920, 1080);
var result = Calculation.GetRawCoordinatesFromPosition(Position.TopLeft, screen, ToolbarWindow, 1.0);
// X: screen.X + offset = 1920 + 24 = 1944
Assert.AreEqual(1944.0, result.X);
// Y: screen.Y + offset = 0 + 24 = 24
Assert.AreEqual(24.0, result.Y);
}
}
}

View File

@@ -0,0 +1,265 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerToys.PowerAccentKeyboardService;
namespace PowerAccent.Core.UnitTests
{
[TestClass]
public class LanguagesTests
{
/// <summary>
/// Product code: Languages.GetDefaultLetterKey
/// What: Verifies that passing an empty language array returns an empty result
/// Why: Guards against NullReferenceException or incorrect fallback when no languages are configured
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_EmptyLanguageArray_ShouldReturnEmpty()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, Array.Empty<Language>());
Assert.AreEqual(0, result.Length);
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.FR
/// What: Verifies that French returns at least one accented character for the letter A
/// Why: Regression guard — ensures the French language mapping is wired up and non-empty
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_SingleLanguage_ShouldReturnNonEmpty()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
Assert.IsTrue(result.Length > 0, "French should have accented characters for A");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with multiple languages
/// What: Verifies that combining French and Spanish merges characters and removes duplicates
/// Why: Guards against duplicate entries when languages share accent characters (e.g., à)
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_MultipleLanguages_ShouldMergeAndDeduplicate()
{
var frResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
var spResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.SP });
var combinedResult = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR, Language.SP });
Assert.AreEqual(combinedResult.Length, combinedResult.Distinct().Count(), "Combined result should contain no duplicates");
foreach (var ch in frResult)
{
CollectionAssert.Contains(combinedResult, ch, $"Combined result should contain French character '{ch}'");
}
foreach (var ch in spResult)
{
CollectionAssert.Contains(combinedResult, ch, $"Combined result should contain Spanish character '{ch}'");
}
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.FR for VK_A
/// What: Verifies that French returns specific expected accent characters (à, â, æ)
/// Why: Pinpoints exact character expectations — catches silent data regressions in language tables
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_French_A_ShouldContainExpectedAccents()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.FR });
CollectionAssert.Contains(result, "à", "French should contain à");
CollectionAssert.Contains(result, "â", "French should contain â");
CollectionAssert.Contains(result, "æ", "French should contain æ");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_O
/// What: Verifies German returns the ö umlaut for the O key
/// Why: Core German character — regression would break German users' workflow
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_German_O_ShouldContainUmlaut()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_O, new[] { Language.DE });
CollectionAssert.Contains(result, "ö", "German should contain ö for O");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_U
/// What: Verifies German returns the ü umlaut for the U key
/// Why: Core German character — regression would break German users' workflow
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_German_U_ShouldContainUmlaut()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_U, new[] { Language.DE });
CollectionAssert.Contains(result, "ü", "German should contain ü for U");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_S
/// What: Verifies German returns the ß (Eszett) for the S key
/// Why: ß is unique to German — its absence would be a critical language regression
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_German_S_ShouldContainEszett()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_S, new[] { Language.DE });
CollectionAssert.Contains(result, "ß", "German should contain ß for S");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.SP for VK_N
/// What: Verifies Spanish returns the ñ character for the N key
/// Why: ñ is the most recognizable Spanish-specific character
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Spanish_N_ShouldContainEne()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_N, new[] { Language.SP });
CollectionAssert.Contains(result, "ñ", "Spanish should contain ñ for N");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.CZ for VK_C
/// What: Verifies Czech returns the č (c-caron) for the C key
/// Why: č is essential for Czech — its absence would break Czech language support
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Czech_C_ShouldContainCaron()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_C, new[] { Language.CZ });
CollectionAssert.Contains(result, "č", "Czech should contain č for C");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.PL for VK_L
/// What: Verifies Polish returns the ł (L with stroke) for the L key
/// Why: ł is fundamental to Polish — its absence would break Polish language support
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Polish_L_ShouldContainStroke()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_L, new[] { Language.PL });
CollectionAssert.Contains(result, "ł", "Polish should contain ł for L");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.VI for VK_A
/// What: Verifies Vietnamese returns a rich set of A variants (≥10 accented forms)
/// Why: Vietnamese has extensive diacritics — guards against truncation of the character table
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Vietnamese_A_ShouldContainMultipleAccents()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_A, new[] { Language.VI });
Assert.IsTrue(result.Length >= 10, "Vietnamese should have many accented A characters");
CollectionAssert.Contains(result, "à", "Vietnamese should contain à");
CollectionAssert.Contains(result, "ă", "Vietnamese should contain ă");
CollectionAssert.Contains(result, "â", "Vietnamese should contain â");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.SPECIAL for VK_0
/// What: Verifies that digit 0 returns superscript (⁰) and subscript (₀) variants
/// Why: Special characters for digits are used in mathematical/scientific input
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Special_Digits_ShouldReturnSubscriptsSuperscripts()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_0, new[] { Language.SPECIAL });
Assert.IsTrue(result.Length > 0, "Special characters for 0 should exist");
CollectionAssert.Contains(result, "⁰", "Special 0 should contain superscript 0");
CollectionAssert.Contains(result, "₀", "Special 0 should contain subscript 0");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.SPECIAL for VK_1
/// What: Verifies that digit 1 returns fraction (½) and superscript (¹) variants
/// Why: Fraction characters are commonly needed for recipes, measurements, etc.
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Special_1_ShouldContainFractions()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_1, new[] { Language.SPECIAL });
CollectionAssert.Contains(result, "½", "Special 1 should contain ½");
CollectionAssert.Contains(result, "¹", "Special 1 should contain ¹");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.CUR
/// What: Verifies that Currency language returns well-known currency symbols (€, $, £)
/// Why: Currency symbols are the primary use case — tests exact symbol presence, not just non-null
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_Currency_ShouldReturnCurrencySymbols()
{
var euroResult = Languages.GetDefaultLetterKey(LetterKey.VK_E, new[] { Language.CUR });
CollectionAssert.Contains(euroResult, "€", "Currency VK_E should contain Euro symbol");
var dollarResult = Languages.GetDefaultLetterKey(LetterKey.VK_S, new[] { Language.CUR });
CollectionAssert.Contains(dollarResult, "$", "Currency VK_S should contain Dollar symbol");
var poundResult = Languages.GetDefaultLetterKey(LetterKey.VK_P, new[] { Language.CUR });
CollectionAssert.Contains(poundResult, "£", "Currency VK_P should contain Pound symbol");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey with Language.DE for VK_Q
/// What: Verifies that a key with no accented characters for a language returns empty
/// Why: Guards against spurious characters appearing for keys that should have no accents
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_UnusedKeyForLanguage_ShouldReturnEmpty()
{
var result = Languages.GetDefaultLetterKey(LetterKey.VK_Q, new[] { Language.DE });
Assert.AreEqual(0, result.Length, "German should have no accented characters for Q");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKeyALL (cache path via ConcurrentDictionary)
/// What: Verifies that calling with all languages twice returns the same cached array instance
/// Why: Cache hit should return the identical object reference, not a recomputed copy
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_AllLanguages_ShouldReturnCachedResults()
{
var allLanguages = Enum.GetValues<Language>();
var result1 = Languages.GetDefaultLetterKey(LetterKey.VK_A, allLanguages);
var result2 = Languages.GetDefaultLetterKey(LetterKey.VK_A, allLanguages);
Assert.IsTrue(result1.Length > 0, "All languages combined should have accented A characters");
CollectionAssert.AreEqual(result1, result2, "Cached results should be identical");
// Verify cache hit returns same object instance, not just equal contents
Assert.AreSame(result1, result2, "Second call should return cached reference");
}
/// <summary>
/// Product code: Languages.GetDefaultLetterKey across all LetterKey × Language combinations
/// What: Smoke-tests every letter key with all languages to ensure no exceptions are thrown
/// Why: Catches missing switch arms or null returns that would crash the toolbar at runtime
/// </summary>
[TestMethod]
public void GetDefaultLetterKey_AllLanguages_EachLetterKey_ShouldNotThrow()
{
var allLanguages = Enum.GetValues<Language>();
var allLetterKeys = Enum.GetValues<LetterKey>();
foreach (var letterKey in allLetterKeys)
{
var result = Languages.GetDefaultLetterKey(letterKey, allLanguages);
Assert.IsNotNull(result, $"Result for {letterKey} should not be null");
Assert.AreEqual(result.Length, result.Distinct().Count(), $"Result for {letterKey} should have no duplicates");
}
}
}
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerAccent.Core.UnitTests
{
[TestClass]
public class PointTests
{
/// <summary>
/// Product code: Point() default constructor
/// What: Verifies X and Y initialize to zero
/// Why: Default state must be deterministic — uninitialized coordinates cause misplacement
/// </summary>
[TestMethod]
public void DefaultConstructor_ShouldInitializeToZero()
{
var point = new Point();
Assert.AreEqual(0, point.X);
Assert.AreEqual(0, point.Y);
}
/// <summary>
/// Product code: Point(double, double) constructor
/// What: Verifies that fractional coordinates are preserved exactly
/// Why: Sub-pixel precision is needed for DPI-aware positioning
/// </summary>
[TestMethod]
public void DoubleConstructor_ShouldSetCoordinates()
{
var point = new Point(3.5, 7.2);
Assert.AreEqual(3.5, point.X);
Assert.AreEqual(7.2, point.Y);
}
/// <summary>
/// Product code: Point(int, int) constructor
/// What: Verifies integer coordinates are stored as doubles without loss
/// Why: Implicit int→double conversion must be exact for pixel-aligned positioning
/// </summary>
[TestMethod]
public void IntConstructor_ShouldSetCoordinates()
{
var point = new Point(10, 20);
Assert.AreEqual(10.0, point.X);
Assert.AreEqual(20.0, point.Y);
}
/// <summary>
/// Product code: Point(System.Drawing.Point) constructor
/// What: Verifies conversion from System.Drawing.Point preserves coordinates
/// Why: Win32 API interop uses System.Drawing.Point — conversion must be lossless
/// </summary>
[TestMethod]
public void DrawingPointConstructor_ShouldConvertCoordinates()
{
var drawingPoint = new System.Drawing.Point(15, 25);
var point = new Point(drawingPoint);
Assert.AreEqual(15.0, point.X);
Assert.AreEqual(25.0, point.Y);
}
/// <summary>
/// Product code: Point implicit operator from System.Drawing.Point
/// What: Verifies implicit conversion works at assignment sites
/// Why: Enables seamless interop without explicit casts in calling code
/// </summary>
[TestMethod]
public void ImplicitConversion_FromDrawingPoint_ShouldWork()
{
Point point = new System.Drawing.Point(100, 200);
Assert.AreEqual(100.0, point.X);
Assert.AreEqual(200.0, point.Y);
}
/// <summary>
/// Product code: Point operator /(Point, double)
/// What: Verifies scalar division halves both coordinates
/// Why: Core arithmetic for DPI normalization
/// </summary>
[TestMethod]
public void DivisionByScalar_ShouldDivideCoordinates()
{
var point = new Point(10.0, 20.0);
var result = point / 2.0;
Assert.AreEqual(5.0, result.X);
Assert.AreEqual(10.0, result.Y);
}
/// <summary>
/// Product code: Point operator /(Point, double) with negative divisor
/// What: Verifies negative divisor negates both coordinates
/// Why: Ensures correct sign propagation in coordinate math
/// </summary>
[TestMethod]
public void DivisionByScalar_WithNegativeDivider_ShouldNegateCoordinates()
{
var point = new Point(10.0, 20.0);
var result = point / -2.0;
Assert.AreEqual(-5.0, result.X);
Assert.AreEqual(-10.0, result.Y);
}
/// <summary>
/// Product code: Point operator /(Point, double) zero guard
/// What: Verifies that dividing by zero throws DivideByZeroException
/// Why: Prevents Infinity coordinates from corrupting toolbar placement
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByScalar_WithZero_ShouldThrow()
{
var point = new Point(10.0, 20.0);
_ = point / 0.0;
}
/// <summary>
/// Product code: Point operator /(Point, Point)
/// What: Verifies component-wise division (X/X, Y/Y)
/// Why: Used for coordinate space transformations
/// </summary>
[TestMethod]
public void DivisionByPoint_ShouldDivideComponentwise()
{
var point = new Point(10.0, 20.0);
var divider = new Point(2.0, 5.0);
var result = point / divider;
Assert.AreEqual(5.0, result.X);
Assert.AreEqual(4.0, result.Y);
}
/// <summary>
/// Product code: Point operator /(Point, Point) zero guard for X
/// What: Verifies that a divider with X=0 throws DivideByZeroException
/// Why: X=0 in a divider would produce Infinity for the X coordinate
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByPoint_WithZeroX_ShouldThrow()
{
var point = new Point(10.0, 20.0);
var divider = new Point(0.0, 5.0);
_ = point / divider;
}
/// <summary>
/// Product code: Point operator /(Point, Point) zero guard for Y
/// What: Verifies that a divider with Y=0 throws DivideByZeroException
/// Why: Y=0 in a divider would produce Infinity for the Y coordinate
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByPoint_WithZeroY_ShouldThrow()
{
var point = new Point(10.0, 20.0);
var divider = new Point(5.0, 0.0);
_ = point / divider;
}
/// <summary>
/// Product code: Point(double, double) constructor with negatives
/// What: Verifies that negative coordinates are stored correctly
/// Why: Negative coordinates are valid in multi-monitor setups (left/above primary)
/// </summary>
[TestMethod]
public void NegativeCoordinates_ShouldBeAllowed()
{
var point = new Point(-5.5, -10.3);
Assert.AreEqual(-5.5, point.X);
Assert.AreEqual(-10.3, point.Y);
}
/// <summary>
/// Product code: Point operator /(Point, double) with fractional divisor
/// What: Verifies dividing by 0.5 effectively doubles coordinates
/// Why: Fractional DPI values (1.25x, 1.5x) are common in production
/// </summary>
[TestMethod]
public void DivisionByScalar_WithFractionalDivider_ShouldWork()
{
var point = new Point(10.0, 20.0);
var result = point / 0.5;
Assert.AreEqual(20.0, result.X);
Assert.AreEqual(40.0, result.Y);
}
/// <summary>
/// Product code: Point operator /(Point, Point) zero guard for both X and Y
/// What: Verifies that a divider with both X=0 and Y=0 throws
/// Why: Degenerate origin-point divider must be rejected — tests the first guard hit
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByPoint_WithBothZero_ShouldThrow()
{
var point = new Point(10.0, 20.0);
var divider = new Point(0.0, 0.0);
_ = point / divider;
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.SelfContained.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\PowerAccent.Core.Tests\</OutputPath>
<RootNamespace>PowerAccent.Core.UnitTests</RootNamespace>
<AssemblyName>PowerToys.PowerAccent.Core.UnitTests</AssemblyName>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerAccent.Core\PowerAccent.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,223 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerAccent.Core.UnitTests
{
[TestClass]
public class RectTests
{
/// <summary>
/// Product code: Rect() default constructor
/// What: Verifies all properties initialize to zero
/// Why: Default state must be well-defined to avoid uninitialized geometry bugs
/// </summary>
[TestMethod]
public void DefaultConstructor_ShouldInitializeToZero()
{
var rect = new Rect();
Assert.AreEqual(0, rect.X);
Assert.AreEqual(0, rect.Y);
Assert.AreEqual(0, rect.Width);
Assert.AreEqual(0, rect.Height);
}
/// <summary>
/// Product code: Rect(int, int, int, int) constructor
/// What: Verifies that integer arguments are correctly stored as doubles
/// Why: Implicit int→double conversion must preserve exact values
/// </summary>
[TestMethod]
public void IntConstructor_ShouldSetAllProperties()
{
var rect = new Rect(10, 20, 800, 600);
Assert.AreEqual(10.0, rect.X);
Assert.AreEqual(20.0, rect.Y);
Assert.AreEqual(800.0, rect.Width);
Assert.AreEqual(600.0, rect.Height);
}
/// <summary>
/// Product code: Rect(double, double, double, double) constructor
/// What: Verifies that fractional double values are stored exactly
/// Why: Ensures no rounding or truncation occurs during construction
/// </summary>
[TestMethod]
public void DoubleConstructor_ShouldSetAllProperties()
{
var rect = new Rect(1.5, 2.5, 100.3, 200.7);
Assert.AreEqual(1.5, rect.X);
Assert.AreEqual(2.5, rect.Y);
Assert.AreEqual(100.3, rect.Width);
Assert.AreEqual(200.7, rect.Height);
}
/// <summary>
/// Product code: Rect(Point, Size) constructor
/// What: Verifies that Point and Size components map to correct Rect properties
/// Why: X/Y come from Point, Width/Height from Size — a swap would silently corrupt layout
/// </summary>
[TestMethod]
public void PointSizeConstructor_ShouldSetFromComponents()
{
var point = new Point(50.0, 100.0);
var size = new Size(400.0, 300.0);
var rect = new Rect(point, size);
Assert.AreEqual(50.0, rect.X);
Assert.AreEqual(100.0, rect.Y);
Assert.AreEqual(400.0, rect.Width);
Assert.AreEqual(300.0, rect.Height);
}
/// <summary>
/// Product code: Rect operator /(Rect, double)
/// What: Verifies component-wise division by a scalar
/// Why: Used for DPI scaling — incorrect division would misposition the toolbar
/// </summary>
[TestMethod]
public void DivisionByScalar_ShouldDivideAllComponents()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
var result = rect / 2.0;
Assert.AreEqual(5.0, result.X);
Assert.AreEqual(10.0, result.Y);
Assert.AreEqual(50.0, result.Width);
Assert.AreEqual(100.0, result.Height);
}
/// <summary>
/// Product code: Rect operator /(Rect, double) zero guard
/// What: Verifies that dividing by zero throws DivideByZeroException
/// Why: Prevents Infinity values from propagating through layout calculations
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByScalar_WithZero_ShouldThrow()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
_ = rect / 0.0;
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect)
/// What: Verifies component-wise division (X/X, Y/Y, Width/Width, Height/Height)
/// Why: Used for coordinate system transformations between screen and window space
/// </summary>
[TestMethod]
public void DivisionByRect_ShouldDivideComponentwise()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
var divider = new Rect(2.0, 5.0, 10.0, 20.0);
var result = rect / divider;
Assert.AreEqual(5.0, result.X);
Assert.AreEqual(4.0, result.Y);
Assert.AreEqual(10.0, result.Width);
Assert.AreEqual(10.0, result.Height);
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect) zero guard for X component
/// What: Verifies that a divider with X=0 throws DivideByZeroException
/// Why: X=0 in a divider Rect would produce Infinity for the X coordinate
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByRect_WithZeroX_ShouldThrow()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
var divider = new Rect(0.0, 5.0, 10.0, 20.0);
_ = rect / divider;
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect) zero guard for Y component
/// What: Verifies that a divider with Y=0 throws DivideByZeroException
/// Why: Y=0 in a divider Rect would produce Infinity for the Y coordinate
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByRect_WithZeroY_ShouldThrow()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
var divider = new Rect(5.0, 0.0, 10.0, 20.0);
_ = rect / divider;
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect) zero guard for Width component
/// What: Verifies that a divider with Width=0 throws DivideByZeroException
/// Why: Guards against producing Infinity values that propagate through calculations
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByRect_WithZeroWidth_ShouldThrow()
{
var rect = new Rect(10, 20, 100, 200);
var divider = new Rect(1, 1, 0, 1);
_ = rect / divider;
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect) zero guard for Height component
/// What: Verifies that a divider with Height=0 throws DivideByZeroException
/// Why: Guards against producing Infinity values that propagate through calculations
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByRect_WithZeroHeight_ShouldThrow()
{
var rect = new Rect(10, 20, 100, 200);
var divider = new Rect(1, 1, 1, 0);
_ = rect / divider;
}
/// <summary>
/// Product code: Rect operator /(Rect, Rect) zero guard for all components
/// What: Verifies that a divider with all zero components throws DivideByZeroException
/// Why: Degenerate all-zero divider must be caught — tests the first guard hit (X=0)
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByRect_WithAllZeroComponents_ShouldThrow()
{
var rect = new Rect(10, 20, 100, 200);
var divider = new Rect(0, 0, 0, 0);
_ = rect / divider;
}
/// <summary>
/// Product code: Rect(double, double, double, double) constructor
/// What: Verifies that negative X/Y coordinates are allowed
/// Why: Multi-monitor setups can have negative screen coordinates (left/above primary)
/// </summary>
[TestMethod]
public void NegativeCoordinates_ShouldBeAllowed()
{
var rect = new Rect(-100.0, -200.0, 400.0, 300.0);
Assert.AreEqual(-100.0, rect.X);
Assert.AreEqual(-200.0, rect.Y);
Assert.AreEqual(400.0, rect.Width);
Assert.AreEqual(300.0, rect.Height);
}
/// <summary>
/// Product code: Rect operator /(Rect, double) with negative divisor
/// What: Verifies that negative divisor negates all components
/// Why: Ensures sign propagation is correct — used in coordinate flipping scenarios
/// </summary>
[TestMethod]
public void DivisionByScalar_WithNegativeDivider_ShouldNegateComponents()
{
var rect = new Rect(10.0, 20.0, 100.0, 200.0);
var result = rect / -1.0;
Assert.AreEqual(-10.0, result.X);
Assert.AreEqual(-20.0, result.Y);
Assert.AreEqual(-100.0, result.Width);
Assert.AreEqual(-200.0, result.Height);
}
}
}

View File

@@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerAccent.Core.UnitTests
{
[TestClass]
public class SizeTests
{
/// <summary>
/// Product code: Size() default constructor
/// What: Verifies Width and Height initialize to zero
/// Why: Default state must be deterministic to avoid layout corruption
/// </summary>
[TestMethod]
public void DefaultConstructor_ShouldInitializeToZero()
{
var size = new Size();
Assert.AreEqual(0, size.Width);
Assert.AreEqual(0, size.Height);
}
/// <summary>
/// Product code: Size(double, double) constructor
/// What: Verifies fractional values are preserved exactly
/// Why: Ensures no rounding during construction for sub-pixel precision
/// </summary>
[TestMethod]
public void DoubleConstructor_ShouldSetDimensions()
{
var size = new Size(100.5, 200.3);
Assert.AreEqual(100.5, size.Width);
Assert.AreEqual(200.3, size.Height);
}
/// <summary>
/// Product code: Size(int, int) constructor
/// What: Verifies integer dimensions are stored as doubles without loss
/// Why: Implicit int→double conversion must be exact
/// </summary>
[TestMethod]
public void IntConstructor_ShouldSetDimensions()
{
var size = new Size(800, 600);
Assert.AreEqual(800.0, size.Width);
Assert.AreEqual(600.0, size.Height);
}
/// <summary>
/// Product code: Size implicit operator from System.Drawing.Size
/// What: Verifies implicit conversion from System.Drawing.Size works
/// Why: Interop boundary — System.Drawing types are used by Win32 APIs
/// </summary>
[TestMethod]
public void ImplicitConversion_FromDrawingSize_ShouldWork()
{
Size size = new System.Drawing.Size(1920, 1080);
Assert.AreEqual(1920.0, size.Width);
Assert.AreEqual(1080.0, size.Height);
}
/// <summary>
/// Product code: Size operator /(Size, double)
/// What: Verifies scalar division halves both dimensions
/// Why: Core arithmetic used for DPI scaling — must produce exact results
/// </summary>
[TestMethod]
public void DivisionByScalar_ShouldDivideDimensions()
{
var size = new Size(100.0, 200.0);
var result = size / 2.0;
Assert.AreEqual(50.0, result.Width);
Assert.AreEqual(100.0, result.Height);
}
/// <summary>
/// Product code: Size operator /(Size, double) with fractional divisor
/// What: Verifies that dividing by 0.5 effectively doubles dimensions
/// Why: Fractional DPI values are real (e.g., 1.25x, 1.5x scaling)
/// </summary>
[TestMethod]
public void DivisionByScalar_WithFractionalDivider_ShouldWork()
{
var size = new Size(100.0, 200.0);
var result = size / 0.5;
Assert.AreEqual(200.0, result.Width);
Assert.AreEqual(400.0, result.Height);
}
/// <summary>
/// Product code: Size operator /(Size, double) zero guard
/// What: Verifies that dividing by zero throws DivideByZeroException
/// Why: Prevents Infinity dimensions that would corrupt window layout
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionByScalar_WithZero_ShouldThrow()
{
var size = new Size(100.0, 200.0);
_ = size / 0.0;
}
/// <summary>
/// Product code: Size operator /(Size, Size)
/// What: Verifies component-wise division (Width/Width, Height/Height)
/// Why: Used for computing scaling ratios between two rectangles
/// </summary>
[TestMethod]
public void DivisionBySize_ShouldDivideComponentwise()
{
var size = new Size(100.0, 200.0);
var divider = new Size(10.0, 20.0);
var result = size / divider;
Assert.AreEqual(10.0, result.Width);
Assert.AreEqual(10.0, result.Height);
}
/// <summary>
/// Product code: Size operator /(Size, Size) zero Width guard
/// What: Verifies that a divider with Width=0 throws DivideByZeroException
/// Why: Width=0 divider would produce Infinity — must be caught early
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionBySize_WithZeroWidth_ShouldThrow()
{
var size = new Size(100.0, 200.0);
var divider = new Size(0.0, 20.0);
_ = size / divider;
}
/// <summary>
/// Product code: Size operator /(Size, Size) zero Height guard
/// What: Verifies that a divider with Height=0 throws DivideByZeroException
/// Why: Height=0 divider would produce Infinity — must be caught early
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionBySize_WithZeroHeight_ShouldThrow()
{
var size = new Size(100.0, 200.0);
var divider = new Size(10.0, 0.0);
_ = size / divider;
}
/// <summary>
/// Product code: Size operator /(Size, double) with negative divisor
/// What: Verifies that negative divisor correctly negates both dimensions
/// Why: Ensures sign propagation is correct in arithmetic
/// </summary>
[TestMethod]
public void DivisionByScalar_WithNegativeDivider_ShouldWork()
{
var size = new Size(100.0, 200.0);
var result = size / -2.0;
Assert.AreEqual(-50.0, result.Width);
Assert.AreEqual(-100.0, result.Height);
}
/// <summary>
/// Product code: Size operator /(Size, double) with very small divisor
/// What: Verifies division by a small number produces expected large result
/// Why: Near-zero divisors (e.g., very low DPI) must not crash or produce NaN
/// </summary>
[TestMethod]
public void DivisionByScalar_WithVerySmallDivider_ShouldYieldLargeResult()
{
var size = new Size(1.0, 1.0);
var result = size / 0.001;
Assert.AreEqual(1000.0, result.Width, 0.01);
Assert.AreEqual(1000.0, result.Height, 0.01);
}
/// <summary>
/// Product code: Size operator /(Size, Size) zero guard for both dimensions
/// What: Verifies that a divider with both Width=0 and Height=0 throws
/// Why: Degenerate zero-size divider must be rejected — tests the first guard hit
/// </summary>
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void DivisionBySize_WithBothZeroDimensions_ShouldThrow()
{
var size = new Size(100.0, 200.0);
var divider = new Size(0.0, 0.0);
_ = size / divider;
}
}
}

View File

@@ -58,7 +58,7 @@ public struct Rect
public static Rect operator /(Rect rect, Rect divider)
{
if (divider.X == 0 || divider.Y == 0)
if (divider.X == 0 || divider.Y == 0 || divider.Width == 0 || divider.Height == 0)
{
throw new DivideByZeroException();
}

View File

@@ -42,7 +42,7 @@ public struct Size
public static Size operator /(Size size, Size divider)
{
if (divider.Width == 0 || divider.Height == 0 || divider.Width == 0 || divider.Height == 0)
if (divider.Width == 0 || divider.Height == 0)
{
throw new DivideByZeroException();
}

View File

@@ -33,4 +33,10 @@
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>PowerToys.PowerAccent.Core.UnitTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -28,11 +28,7 @@
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<!-- System.Diagnostics.EventLog is now provided to all C# projects via Directory.Build.props -->
</ItemGroup>
<ItemGroup>