Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2341,3 +2341,8 @@ YTimer
zamora
zonability
Zorder
aaaa
hpa
LOCALDISPLAY
LShaped
normalises
4 changes: 4 additions & 0 deletions PowerToys.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/CursorWrap/UnitTests/UnitTests-CursorWrap.vcxproj" Id="c1f0fe89-a695-472e-9df5-f793b88ee220" />
<Project Path="src/modules/MouseUtils/MouseHighlighter/UnitTests/UnitTests-MouseHighlighter.vcxproj" Id="753db0e7-1670-4cdf-90c9-cc8316593410" />
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/UnitTests/UnitTests-Crosshairs.vcxproj" Id="9f01b331-8518-4d02-b8c2-8f7415c7742f" />
</Folder>
<Folder Name="/modules/keyboardmanager/Tests/">
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" />
Expand Down Expand Up @@ -1110,5 +1113,6 @@
<BuildDependency Project="src/PackageIdentity/PackageIdentity.vcxproj" />
</Project>
<Project Path="src/Update/PowerToys.Update.vcxproj" Id="44ce9ae1-4390-42c5-bacc-0fd6b40aa203" />
<Project Path="src/runner/UnitTests/UnitTests-Runner.vcxproj" Id="97bdacf8-261d-4e23-a708-a27e0b60e444" />
<Project Path="tools/project_template/ModuleTemplate/ModuleTemplateCompileTest.vcxproj" Id="64a80062-4d8b-4229-8a38-dfa1d7497749" />
</Solution>
367 changes: 367 additions & 0 deletions src/modules/MouseUtils/CursorWrap/UnitTests/TopologyTests.cpp

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<Import Project="$(RepoRoot)deps\spdlog.props" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{C1F0FE89-A695-472E-9DF5-F793B88EE220}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UnitTestsCursorWrap</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>CursorWrap.UnitTests</ProjectName>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>

<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UnitTestsCursorWrap\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\..\..\;..\..\..\..\common\Telemetry;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>RuntimeObject.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="TopologyTests.cpp" />
<ClCompile Include="..\MonitorTopology.cpp" />
<ClCompile Include="..\CursorWrapCore.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="..\MonitorTopology.h" />
<ClInclude Include="..\CursorWrapCore.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(RepoRoot)src\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="TopologyTests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\MonitorTopology.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\CursorWrapCore.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\MonitorTopology.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\CursorWrapCore.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>
4 changes: 4 additions & 0 deletions src/modules/MouseUtils/CursorWrap/UnitTests/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
</packages>
5 changes: 5 additions & 0 deletions src/modules/MouseUtils/CursorWrap/UnitTests/pch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header

#include "pch.h"

// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
20 changes: 20 additions & 0 deletions src/modules/MouseUtils/CursorWrap/UnitTests/pch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#ifndef PCH_H
#define PCH_H

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#include <atomic>
#include <thread>
#include <vector>
#include <map>
#include <string>
#include <sstream>
#include <iomanip>
#include <ctime>
#include <algorithm>
#include <cmath>

#endif // PCH_H
181 changes: 181 additions & 0 deletions src/modules/MouseUtils/MouseHighlighter/UnitTests/HighlighterTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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.

// Unit tests for MouseHighlighter lifecycle logic, mirroring the Rust test suite
// in src/rust/libs/highlighter-core/src/highlight_manager.rs.
//
// The C++ implementation drives highlights through Windows Composition APIs,
// but the settings and colour/fade constants are testable without a window.
// These tests verify defaults, fade timing arithmetic, and the alpha==0
// disabled behavior at the configuration level.

#include "pch.h"

#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)

#include "../MouseHighlighter.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

namespace MouseHighlighterUnitTests
{
TEST_CLASS(HighlighterTests)
{
public:
// ── Default settings ────────────────────────────────────────────

// Left-click highlight should be semi-transparent yellow (a=166, r=255, g=255, b=0).
TEST_METHOD(LeftClick_DefaultColor_IsYellow)
{
MouseHighlighterSettings settings;
auto c = settings.leftButtonColor;
Assert::AreEqual(static_cast<uint8_t>(166), c.A, L"Alpha should be 166");
Assert::AreEqual(static_cast<uint8_t>(255), c.R, L"Red should be 255");
Assert::AreEqual(static_cast<uint8_t>(255), c.G, L"Green should be 255");
Assert::AreEqual(static_cast<uint8_t>(0), c.B, L"Blue should be 0");
}

// Right-click highlight should be semi-transparent blue (a=166, r=0, g=0, b=255).
TEST_METHOD(RightClick_DefaultColor_IsBlue)
{
MouseHighlighterSettings settings;
auto c = settings.rightButtonColor;
Assert::AreEqual(static_cast<uint8_t>(166), c.A);
Assert::AreEqual(static_cast<uint8_t>(0), c.R);
Assert::AreEqual(static_cast<uint8_t>(0), c.G);
Assert::AreEqual(static_cast<uint8_t>(255), c.B);
}

// ── Fade timing constants ───────────────────────────────────────

// The fade delay should default to 500ms.
TEST_METHOD(FadeDelay_Default_Is500ms)
{
MouseHighlighterSettings settings;
Assert::AreEqual(MOUSE_HIGHLIGHTER_DEFAULT_DELAY_MS, settings.fadeDelayMs);
Assert::AreEqual(500, settings.fadeDelayMs);
}

// The fade duration should default to 250ms.
TEST_METHOD(FadeDuration_Default_Is250ms)
{
MouseHighlighterSettings settings;
Assert::AreEqual(MOUSE_HIGHLIGHTER_DEFAULT_DURATION_MS, settings.fadeDurationMs);
Assert::AreEqual(250, settings.fadeDurationMs);
}

// Verify the full fade lifecycle timing:
// Press → Release → (delay)500ms → (duration)250ms → fully transparent.
// A highlight released at t=0 should be fully opaque at t=499,
// mid-fade at t=625, and gone at t=750.
TEST_METHOD(FadeDelay_HoldsOpacity)
{
MouseHighlighterSettings settings;
// 500ms delay means highlight stays at full opacity for 500ms
// after release before starting to fade.
int totalVisibleMs = settings.fadeDelayMs + settings.fadeDurationMs;
Assert::AreEqual(750, totalVisibleMs,
L"Total visible time should be delay + duration = 750ms");
}

TEST_METHOD(FadeDuration_ReducesToZero)
{
MouseHighlighterSettings settings;
// At the end of delay + duration the opacity should be 0.
// Opacity formula (from Rust):
// elapsed = now - fade_started_at
// if elapsed < delay: 1.0
// else: 1.0 - (elapsed - delay) / duration
// clamped to [0.0, 1.0]
// At elapsed == delay + duration: 1.0 - duration/duration = 0.0
int elapsed = settings.fadeDelayMs + settings.fadeDurationMs;
double opacity = 1.0 - (static_cast<double>(elapsed) - static_cast<double>(settings.fadeDelayMs)) / static_cast<double>(settings.fadeDurationMs);
Assert::AreEqual(0.0, opacity, 1e-10,
L"Opacity should be 0.0 at delay+duration");
}

// ── Alpha=0 disables button highlight ───────────────────────────

// If the alpha channel of the always-colour is 0 (default), that
// highlight source is disabled.
TEST_METHOD(AlphaZero_DisablesAlwaysHighlight)
{
MouseHighlighterSettings settings;
Assert::AreEqual(static_cast<uint8_t>(0), settings.alwaysColor.A,
L"Default always-colour alpha should be 0 (disabled)");
}

// A custom left-button colour with alpha=0 should be considered disabled.
TEST_METHOD(AlphaZero_DisablesLeftButton)
{
MouseHighlighterSettings settings;
settings.leftButtonColor = winrt::Windows::UI::ColorHelper::FromArgb(0, 255, 255, 0);
Assert::AreEqual(static_cast<uint8_t>(0), settings.leftButtonColor.A,
L"Alpha=0 should disable this button's highlight");
}

// A custom right-button colour with alpha=0 should be considered disabled.
TEST_METHOD(AlphaZero_DisablesRightButton)
{
MouseHighlighterSettings settings;
settings.rightButtonColor = winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 255);
Assert::AreEqual(static_cast<uint8_t>(0), settings.rightButtonColor.A,
L"Alpha=0 should disable right button highlight");
}

// ── Multiple-click highlight arithmetic ─────────────────────────

// Each click should create an independent highlight. We test the
// settings don't restrict the number of concurrent highlights.
TEST_METHOD(MultipleClicks_SettingsAllowMultiple)
{
// The implementation uses a per-click visual; there is no cap
// in MouseHighlighterSettings. Verify by constructing settings
// and checking radius > 0 (i.e., highlights are visible).
MouseHighlighterSettings settings;
Assert::IsTrue(settings.radius > 0,
L"Radius should be positive to allow visible highlights");
}

// ── Cleanup: expired highlights ─────────────────────────────────

// After the fade is complete (delay + duration), the highlight should
// be removed. We test the arithmetic that determines expiry.
TEST_METHOD(Cleanup_ExpirationArithmetic)
{
MouseHighlighterSettings settings;
int fadeStartMs = 1000;
int nowMs = fadeStartMs + settings.fadeDelayMs + settings.fadeDurationMs + 1;

// Elapsed since fade started.
int elapsed = nowMs - fadeStartMs;
bool expired = elapsed > (settings.fadeDelayMs + settings.fadeDurationMs);
Assert::IsTrue(expired,
L"Highlight should be expired after delay + duration");
}

// ── Default values ──────────────────────────────────────────────

TEST_METHOD(DefaultRadius_Is20)
{
MouseHighlighterSettings settings;
Assert::AreEqual(20, settings.radius);
}

TEST_METHOD(DefaultAutoActivate_IsFalse)
{
MouseHighlighterSettings settings;
Assert::IsFalse(settings.autoActivate);
}

TEST_METHOD(DefaultSpotlightMode_IsFalse)
{
MouseHighlighterSettings settings;
Assert::IsFalse(settings.spotlightMode);
}
};
}
Loading
Loading