Compare commits

..

1 Commits

Author SHA1 Message Date
Gordon Lam (SH)
d868a85b87 Add Version 2025-06-13 18:53:02 +08:00
161 changed files with 1748 additions and 9129 deletions

View File

@@ -156,7 +156,6 @@ builttoroam
BVal
BValue
byapp
BYCOMMAND
BYPOSITION
CALCRECT
CALG
@@ -192,7 +191,6 @@ CImage
cla
CLASSDC
CLASSNOTAVAILABLE
cleanmgr
clickable
clickonce
CLIENTEDGE
@@ -277,7 +275,6 @@ currentculture
CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
customaction
CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER
@@ -345,9 +342,7 @@ DESELECTOTHERS
DESIGNINFO
DESKTOPABSOLUTEEDITING
DESKTOPABSOLUTEPARSING
DESKTOPHORZRES
desktopshorcutinstalled
DESKTOPVERTRES
devblogs
devdocs
devmgmt
@@ -358,7 +353,6 @@ DFX
DIALOGEX
digicert
dimm
DINORMAL
DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
@@ -402,7 +396,6 @@ DVASPECTINFO
DVD
dvr
DVTARGETDEVICE
dwflags
dwl
dwm
dwmapi
@@ -491,7 +484,6 @@ FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fff
FFFF
FILEEXPLORER
FILEFLAGS
FILEFLAGSMASK
@@ -610,7 +602,6 @@ helptext
HGFE
hglobal
hhk
HHmmssfff
hhx
Hiber
Hiberboot
@@ -619,7 +610,6 @@ hicon
HIDEREADONLY
HIDEWINDOW
Hif
hightlight
HIMAGELIST
himl
hinst
@@ -754,7 +744,6 @@ isocpp
iss
issecret
ISSEPARATOR
istep
ith
ITHUMBNAIL
IUI
@@ -1285,7 +1274,6 @@ prvpane
psapi
pscid
PSECURITY
psexec
psfgao
psfi
PSMODULEPATH
@@ -1543,7 +1531,6 @@ SLGP
sln
SMALLICON
smartphone
smileys
SMTO
SNAPPROCESS
snk
@@ -1676,7 +1663,6 @@ TDefault
TDevice
telephon
templatenamespace
TESTONLY
testprocess
TEXCOORD
TEXTBOXNEWLINE
@@ -1729,7 +1715,6 @@ trx
tsa
TSender
TServer
tskill
tstoi
TStr
tweakme
@@ -1888,7 +1873,6 @@ WINDOWPOSCHANGING
WINDOWSBUILDNUMBER
windowssearch
windowssettings
windowsterminal
WINDOWSTYLES
WINDOWSTYLESICON
winerror
@@ -1998,7 +1982,6 @@ Zoneszonabletester
Zoomin
zoomit
ZOOMITX
Zorder
ZXk
ZXNs
zzz

View File

@@ -53,9 +53,6 @@ parameters:
- name: runTests
type: boolean
default: true
- name: buildTests
type: boolean
default: true
- name: useVSPreview
type: boolean
default: false
@@ -113,7 +110,7 @@ jobs:
JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }}
NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=$(BuildPlatform) # Required for nuget to work due to self contained
NODE_OPTIONS: --max_old_space_size=16384
${{ if or(eq(parameters.runTests, true), eq(parameters.buildTests, true)) }}:
${{ if eq(parameters.runTests, true) }}:
MSBuildMainBuildTargets: Build;Test
${{ else }}:
MSBuildMainBuildTargets: Build
@@ -397,7 +394,10 @@ jobs:
testAssemblyVer2: |
**\KeyboardManagerEngineTest.dll
**\KeyboardManagerEditorTest.dll
**\*UnitTest*.dll
**\UnitTests-CommonLib.dll
**\PowerRenameUnitTests.dll
**\UnitTests-FancyZones.dll
**\\WorkspacesLibUnitTests.dll
!**\obj\**
- pwsh: |-
@@ -531,7 +531,7 @@ jobs:
displayName: Stage GPO files
# Running the tests may result in future jobs consuming artifacts out of this build
- ${{ if or(eq(parameters.runTests, true), eq(parameters.buildTests, true)) }}:
- ${{ if eq(parameters.runTests, true) }}:
- task: CopyFiles@2
displayName: Stage entire build output
inputs:

View File

@@ -15,29 +15,20 @@ parameters:
jobs:
- job: Test${{ parameters.platform }}${{ parameters.configuration }}
displayName: Test ${{ parameters.platform }} ${{ parameters.configuration }}
timeoutInMinutes: 300
variables:
${{ if or(eq(parameters.platform, 'x64Win10'), eq(parameters.platform, 'x64Win11')) }}:
BuildPlatform: x64
${{ else }}:
BuildPlatform: ${{ parameters.platform }}
TestPlatform: ${{ parameters.platform }}
BuildPlatform: ${{ parameters.platform }}
BuildConfiguration: ${{ parameters.configuration }}
SrcPath: $(Build.Repository.LocalPath)
TestArtifactsName: build-${{ variables.BuildPlatform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
TestArtifactsName: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
${{ if ne(parameters.platform, 'ARM64') }}:
name: SHINE-INT-Testing-x64
${{ if eq(parameters.platform, 'x64Win11') }}:
demands: ImageOverride -equals SHINE-W11-Testing
${{ else }}:
name: SHINE-INT-Testing-arm64
${{ else }}:
${{ if ne(parameters.platform, 'ARM64') }}:
name: SHINE-OSS-Testing-x64
${{ if eq(parameters.platform, 'x64Win11') }}:
demands: ImageOverride -equals SHINE-W11-Testing
${{ else }}:
name: SHINE-OSS-Testing-arm64
steps:
@@ -110,13 +101,8 @@ jobs:
vsTestVersion: 'toolsInstaller'
uiTests: true
rerunFailedTests: true
# Since UITests-FancyZonesEditor.dll is generated in both UITests-FancyZonesEditor and UITests-FancyZones, removed one to avoid duplicate test runs
testAssemblyVer2: |
**\*UITest*.dll
**\UITests-FancyZones.dll
**\UITests-FancyZonesEditor.dll
!**\obj\**
!**\ref\**
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
env:
platform: '$(TestPlatform)'
!**\ref\**

View File

@@ -60,4 +60,16 @@ stages:
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
- ${{ if and(eq(parameters.runTests, true), not(and(eq(platform, 'arm64'), eq(variables['System.PullRequest.IsFork'], true)))) }}:
- stage: Test_${{ platform }}
displayName: Test ${{ platform }}
dependsOn:
- Build_${{platform}}
jobs:
- template: job-test-project.yml
parameters:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}

View File

@@ -1,84 +0,0 @@
variables:
- name: runCodesignValidationInjectionBG
value: false
- name: EnablePipelineCache
value: true
- ${{ if eq(parameters.enableMsBuildCaching, true) }}:
- name: EnablePipelineCache
value: true
parameters:
- name: buildPlatforms
type: object
default:
- x64
- arm64
- name: enableMsBuildCaching
type: boolean
default: false
- name: useVSPreview
type: boolean
default: false
- name: useLatestWebView2
type: boolean
default: false
stages:
- ${{ each platform in parameters.buildPlatforms }}:
- stage: Build_${{ platform }}
displayName: Build ${{ platform }}
dependsOn: []
jobs:
- template: job-build-project.yml
parameters:
pool:
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
buildPlatforms:
- ${{ platform }}
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
runTests: false
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
- ${{ if eq(platform, 'x64') }}:
- stage: Test_x64Win10
displayName: Test x64Win10
dependsOn:
- Build_${{platform}}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win10
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
- ${{ if eq(platform, 'x64') }}:
- stage: Test_x64Win11
displayName: Test x64Win11
dependsOn:
- Build_${{platform}}
jobs:
- template: job-test-project.yml
parameters:
platform: x64Win11
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}
- ${{ if ne(platform, 'x64') }}:
- stage: Test_${{ platform }}
displayName: Test ${{ platform }}
dependsOn:
- Build_${{platform}}
jobs:
- template: job-test-project.yml
parameters:
platform: ${{ platform }}
configuration: Release
useLatestWebView2: ${{ parameters.useLatestWebView2 }}

View File

@@ -92,3 +92,4 @@ if ($totalFailures -gt 0) {
Write-Host -ForegroundColor Green "All " $referencedFileVersionsPerDll.keys.Count " libraries are mentioned with the same version across the dependencies.`r`n"
exit 0

View File

@@ -28,8 +28,6 @@ $versionExceptions = @(
"ObjectModelCsProjection.dll",
"RendererCsProjection.dll") -join '|';
$nullVersionExceptions = @(
"SkiaSharp.Views.WinUI.Native.dll",
"libSkiaSharp.dll",
"codicon.ttf",
"e_sqlite3.dll",
"getfilesiginforedist.dll",

View File

@@ -181,6 +181,7 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
## PowerToys core team
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Lead
- [@nguyen-dows](https://github.com/nguyen-dows) - Christopher Nguyen - Product Manager
- [@craigloewen-msft](https://github.com/craigloewen-msft) - Craig Loewen - Product Manager
- [@niels9001](https://github.com/niels9001/) - Niels Laute - Product Manager
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev lead
@@ -212,7 +213,6 @@ ZoomIt source code was originally implemented by [Sysinternals](https://sysinter
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager
- [@nguyen-dows](https://github.com/nguyen-dows) - Christopher Nguyen - Product Manager
- [@enricogior](https://github.com/enricogior) - Enrico Giordani - Dev Lead
- [@bzoz](https://github.com/bzoz) - Bartosz Sosnowski - Dev
- [@ivan100sic](https://github.com/ivan100sic) - Ivan Stošić - Dev

View File

@@ -633,6 +633,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}"
@@ -715,14 +717,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "sr
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesEditorUITest", "src\modules\Workspaces\WorkspacesEditorUITest\WorkspacesEditorUITest.csproj", "{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalculatorEngineCommon", "src\common\CalculatorEngineCommon\CalculatorEngineCommon.vcxproj", "{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2299,6 +2295,14 @@ Global
{E09AA983-C755-474F-83D6-A5CDF528C070}.Release|ARM64.Build.0 = Release|ARM64
{E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.ActiveCfg = Release|x64
{E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.Build.0 = Release|x64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.Build.0 = Debug|ARM64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.ActiveCfg = Debug|x64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.Build.0 = Debug|x64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.ActiveCfg = Release|ARM64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.Build.0 = Release|ARM64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.ActiveCfg = Release|x64
{6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.Build.0 = Release|x64
{92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.Build.0 = Debug|ARM64
{92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|x64.ActiveCfg = Debug|x64
@@ -2595,22 +2599,6 @@ Global
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64
{64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.Build.0 = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.Build.0 = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.ActiveCfg = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.Build.0 = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.ActiveCfg = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.Build.0 = Debug|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.ActiveCfg = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.Build.0 = Debug|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.ActiveCfg = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
@@ -2641,14 +2629,6 @@ Global
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|ARM64.Build.0 = Release|ARM64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|x64.ActiveCfg = Release|x64
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|x64.Build.0 = Release|x64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Debug|ARM64.Build.0 = Debug|ARM64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Debug|x64.ActiveCfg = Debug|x64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Debug|x64.Build.0 = Debug|x64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Release|ARM64.ActiveCfg = Release|ARM64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Release|ARM64.Build.0 = Release|ARM64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Release|x64.ActiveCfg = Release|x64
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2788,7 +2768,7 @@ Global
{25C91A4E-BA4E-467A-85CD-8B62545BF674} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{6AB6A2D6-F859-4A82-9184-0BD29C9F07D1} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{212AD910-8488-4036-BE20-326931B75FB2} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{7AC943C9-52E8-44CF-9083-744D8049667B} = {322566EF-20DC-43A6-B9F8-616AF942579A}
{7AC943C9-52E8-44CF-9083-744D8049667B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B}
@@ -2881,6 +2861,7 @@ Global
{ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{6CE438DF-C245-4997-A360-0A0939E4BA34} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{E09AA983-C755-474F-83D6-A5CDF528C070} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{6D56B64D-FF1F-488F-AFED-9B9854A5D399} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{92EC89E4-9972-453A-8A1A-3A9E230C146A} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{51939B4F-1F62-4BFF-A6A2-C08646E5BE95} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{D1160404-D3D1-497A-883A-4059C07C2273} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
@@ -2919,13 +2900,10 @@ Global
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {322566EF-20DC-43A6-B9F8-616AF942579A}
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
{0217E86E-3476-9946-DE8E-9D200CEBD47A} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{E69B16BE-278F-3A09-394C-CCFF3F3ECE12} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -21,74 +21,67 @@
- Create a new project and add the following references to the project file. Change the OutputPath to your own module's path.
```
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}</ProjectGuid>
<RootNamespace>PowerToys.Hosts.UITests</RootNamespace>
<AssemblyName>PowerToys.Hosts.UITests</AssemblyName>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>
<PropertyGroup>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\KeyboardManagerUITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
<Folder Include="Properties\" />
</ItemGroup>
```
- Inherit your test class from UITestBase.
>Set Scope: The default scope starts from the PowerToys settings UI. If you want to start from your own module, set the constructor as shown below:
>Specify Scope:
```
[TestClass]
public class HostModuleTests : UITestBase
{
public HostModuleTests()
: base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
{
}
}
[TestClass]
public class RunFancyZonesTest : UITestBase
{
public RunFancyZonesTest()
: base(PowerToysModule.FancyZone)
{
}
}
```
- Then you can start performing the UI operations.
- Then you can start using session to perform the UI operations.
**Example**
```
[TestMethod("Hosts.Basic.EmptyViewShouldWork")]
[TestCategory("Hosts File Editor #4")]
public void TestEmptyView()
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UITests_KeyboardManager
{
this.CloseWarningDialog();
this.RemoveAllEntries();
[TestClass]
public class RunKeyboardManagerUITests : UITestBase
{
[TestMethod]
public void OpenKeyboardManagerEditor()
{
// Open KeyboardManagerEditor
this.Session.Find<Button>(By.Name("Remap a key")).Click();
this.Session.Attach("Remap keys");
// 'Add an entry' button (only show-up when list is empty) should be visible
Assert.IsTrue(this.HasOne<HyperlinkButton>("Add an entry"), "'Add an entry' button should be visible in the empty view");
// Maximize window
var window = Session.Find<Window>(By.Name("Remap keys")).Maximize();
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "EmptyView");
// Add Key Remapping
this.Session.Find<Button>(By.Name("Add key remapping")).Click();
window.Close();
// Click 'Add an entry' from empty-view for adding Host override rule
this.Find<HyperlinkButton>("Add an entry").Click();
this.AddEntry("192.168.0.1", "localhost", false, false);
// Should have one row now and not more empty view
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
Assert.IsFalse(this.Has<HyperlinkButton>("Add an entry"), "'Add an entry' button should be invisible if not empty view");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "NonEmptyView");
// Back to Settings
this.Session.Attach(PowerToysModule.PowerToysSettings);
}
}
}
```

View File

@@ -1,36 +0,0 @@
#include <windows.h>
#include "resource.h"
#include "../version/version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -98,11 +98,6 @@
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ProjectReference Include="..\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ExprtkEvaluator.h" />
<ClInclude Include="pch.h" />
@@ -110,7 +105,6 @@
<ClInclude Include="Calculator.h">
<DependentUpon>Calculator.idl</DependentUpon>
</ClInclude>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="ExprtkEvaluator.cpp">
@@ -134,14 +128,16 @@
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="CalculatorEngineCommon.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<ItemGroup>
<ProjectReference Include="..\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<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>

View File

@@ -1,13 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by CalculatorEngineCommon.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "CalculatorEngineCommon"
#define INTERNAL_NAME "CalculatorEngineCommon"
#define ORIGINAL_FILENAME "CalculatorEngineCommon.dll"
// Non-localizable
//////////////////////////////

View File

@@ -29,7 +29,9 @@ namespace ManagedCommon
* If you want to publish it with Native AOT enabled (or publish as a single file).
* You need to find another way to remove Assembly.Location usage.
*/
private static readonly string Version = "2.0.0";
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
/// <summary>
/// Initializes the logger and sets the path for logging.

View File

@@ -37,6 +37,17 @@ namespace Microsoft.PowerToys.Telemetry
public void WriteEvent<T>(T telemetryEvent)
where T : EventBase, IEvent
{
if (DataDiagnosticsSettings.GetEnabledValue())
{
this.Write<T>(
telemetryEvent.EventName,
new EventSourceOptions()
{
Keywords = ProjectKeywordMeasure,
Tags = ProjectTelemetryTagProductAndServicePerformance,
},
telemetryEvent);
}
}
}
}

View File

@@ -1,53 +0,0 @@
// 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.
namespace Microsoft.PowerToys.UITest
{
public class CheckBox : Element
{
private static readonly string ExpectedControlType = "ControlType.CheckBox";
/// <summary>
/// Initializes a new instance of the <see cref="CheckBox"/> class.
/// </summary>
public CheckBox()
{
this.TargetControlType = CheckBox.ExpectedControlType;
}
/// <summary>
/// Select the item of the ComboBox.
/// </summary>
/// <param name="value">The text to select from the list view.</param>
public void Select(string value)
{
this.Find<NavigationViewItem>(value).Click();
}
/// <summary>
/// Gets a value indicating whether the CheckBox is checked.
/// </summary>
public bool IsChecked => this.Selected;
public CheckBox SetCheck(bool value = true, int msPreAction = 500, int msPostAction = 500)
{
if (this.IsChecked != value)
{
if (msPreAction > 0)
{
Task.Delay(msPreAction).Wait();
}
// Toggle the switch
this.Click();
if (msPostAction > 0)
{
Task.Delay(msPostAction).Wait();
}
}
return this;
}
}
}

View File

@@ -1,28 +0,0 @@
// 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.
namespace Microsoft.PowerToys.UITest
{
public class ComboBox : Element
{
private static readonly string ExpectedControlType = "ControlType.ComboBox";
/// <summary>
/// Initializes a new instance of the <see cref="ComboBox"/> class.
/// </summary>
public ComboBox()
{
this.TargetControlType = ComboBox.ExpectedControlType;
}
/// <summary>
/// Select the item of the ComboBox.
/// </summary>
/// <param name="value">The text to select from the list view.</param>
public void Select(string value)
{
this.Find<NavigationViewItem>(value).Click();
}
}
}

View File

@@ -1,74 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.UITest
{
public class Custom : Element
{
private static readonly string ExpectedControlType = "ControlType.Custom";
/// <summary>
/// Initializes a new instance of the <see cref="Custom"/> class.
/// </summary>
public Custom()
{
this.TargetControlType = Custom.ExpectedControlType;
}
/// <summary>
/// Sends a combination of keys.
/// </summary>
/// <param name="keys">The keys to send.</param>
public void SendKeys(params Key[] keys)
{
PerformAction((actions, windowElement) =>
{
KeyboardHelper.SendKeys(keys);
});
}
/// <summary>
/// Drag element move offset.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
/// <summary>
/// Drag element move to other element.
/// </summary>
/// <param name="element">Move to this element.</param>
public void Drag(Element element)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).ClickAndHold();
Assert.IsNotNull(element.WindowsElement, "element is null");
int dx = (element.WindowsElement.Rect.X - windowElement.Rect.X) / 10;
int dy = (element.WindowsElement.Rect.Y - windowElement.Rect.Y) / 10;
for (int i = 0; i < 10; i++)
{
actions.MoveByOffset(dx, dy);
}
actions.Release();
actions.Build().Perform();
});
}
}
}

View File

@@ -7,7 +7,6 @@ using System.Drawing;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using ABI.Windows.Foundation;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
@@ -26,20 +25,8 @@ namespace Microsoft.PowerToys.UITest
{
private WindowsElement? windowsElement;
protected internal WindowsElement? WindowsElement
{
get => windowsElement;
set => windowsElement = value;
}
private WindowsDriver<WindowsElement>? driver;
protected internal WindowsDriver<WindowsElement>? Driver
{
get => driver;
set => driver = value;
}
protected string? TargetControlType { get; set; }
internal bool IsMatchingTarget()
@@ -125,12 +112,9 @@ namespace Microsoft.PowerToys.UITest
/// Click the UI element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
/// <param name="msPreAction">Delay in milliseconds before performing the click action. Default is 500 ms.</param>
/// <param name="msPostAction">Delay in milliseconds after performing the click action. Default is 500 ms.</param>
public virtual void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
public virtual void Click(bool rightClick = false)
{
PerformAction(
(actions, windowElement) =>
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement);
@@ -147,9 +131,7 @@ namespace Microsoft.PowerToys.UITest
}
actions.Build().Perform();
},
msPreAction,
msPostAction);
});
}
/// <summary>
@@ -170,20 +152,51 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Release action
/// Drag element move offset.
/// </summary>
public void ReleaseAction()
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
var releaseAction = new Actions(driver);
releaseAction.Release().Perform();
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
/// <summary>
/// Release key
/// Drag element move to other element.
/// </summary>
public void ReleaseKey(Key key)
/// <param name="element">Move to this element.</param>
public void Drag(Element element)
{
KeyboardHelper.ReleaseKey(key);
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).ClickAndHold();
Assert.IsNotNull(element.windowsElement, "element is null");
int dx = (element.windowsElement.Rect.X - windowElement.Rect.X) / 10;
int dy = (element.windowsElement.Rect.Y - windowElement.Rect.Y) / 10;
for (int i = 0; i < 10; i++)
{
actions.MoveByOffset(dx, dy);
}
actions.Release();
actions.Build().Perform();
});
}
/// <summary>
/// Send Key of the element.
/// </summary>
/// <param name="key">The Key to Send.</param>
public void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(key);
});
}
/// <summary>
@@ -205,7 +218,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to use for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public T Find<T>(By by, int timeoutMS = 5000)
public T Find<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -213,7 +226,7 @@ namespace Microsoft.PowerToys.UITest
// leverage findAll to filter out mismatched elements
var collection = this.FindAll<T>(by, timeoutMS);
Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
return collection[0];
}
@@ -226,7 +239,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public T Find<T>(string name, int timeoutMS = 5000)
public T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Find<T>(By.Name(name), timeoutMS);
@@ -239,7 +252,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to use for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(By by, int timeoutMS = 5000)
public Element Find(By by, int timeoutMS = 3000)
{
return this.Find<Element>(by, timeoutMS);
}
@@ -251,7 +264,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(string name, int timeoutMS = 5000)
public Element Find(string name, int timeoutMS = 3000)
{
return this.Find<Element>(By.Name(name), timeoutMS);
}
@@ -263,7 +276,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000)
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
@@ -295,7 +308,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000)
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.FindAll<T>(By.Name(name), timeoutMS);
@@ -308,7 +321,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000)
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.FindAll<Element>(by, timeoutMS);
}
@@ -320,23 +333,11 @@ namespace Microsoft.PowerToys.UITest
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000)
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Send Key of the element.
/// </summary>
/// <param name="key">The Key to Send.</param>
protected void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(key);
});
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
@@ -359,15 +360,5 @@ namespace Microsoft.PowerToys.UITest
Task.Delay(msPostAction).Wait();
}
}
/// <summary>
/// Save UI Element to a PNG file.
/// </summary>
/// <param name="path">the full path</param>
internal void SaveToPngFile(string path)
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToPngFile with parameter: path = {path}");
this.windowsElement.GetScreenshot().SaveAsFile(path);
}
}
}

View File

@@ -1,25 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
public class Group : Element
{
private static readonly string ExpectedControlType = "ControlType.Group";
/// <summary>
/// Initializes a new instance of the <see cref="Group"/> class.
/// </summary>
public Group()
{
this.TargetControlType = Group.ExpectedControlType;
}
}
}

View File

@@ -24,12 +24,9 @@ namespace Microsoft.PowerToys.UITest
/// Click the ListItem element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
/// <param name="msPreAction">Pre action delay in milliseconds. Default value is 500</param>
/// <param name="msPostAction">Post action delay in milliseconds. Default value is 500</param>
public override void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
public override void Click(bool rightClick = false)
{
PerformAction(
(actions, windowElement) =>
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement, 10, 10);
@@ -43,36 +40,7 @@ namespace Microsoft.PowerToys.UITest
}
actions.Build().Perform();
},
msPreAction,
msPostAction);
}
/// <summary>
/// Click the center of the ListItem element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
/// <param name="msPreAction">Pre action delay in milliseconds. Default value is 500</param>
/// <param name="msPostAction">Post action delay in milliseconds. Default value is 500</param>
public void ClickCenter(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
{
PerformAction(
(actions, windowElement) =>
{
actions.MoveToElement(windowElement);
if (rightClick)
{
actions.ContextClick();
}
else
{
actions.Click();
}
actions.Build().Perform();
},
msPreAction,
msPostAction);
});
}
/// <summary>

View File

@@ -1,101 +0,0 @@
// 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 OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
public class Pane : Element
{
private static readonly string ExpectedControlType = "ControlType.Pane";
/// <summary>
/// Initializes a new instance of the <see cref="Pane"/> class.
/// </summary>
public Pane()
{
this.TargetControlType = Pane.ExpectedControlType;
}
/// <summary>
/// Drag element move offset.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
/// <summary>
/// Simulates holding when dragging to target position.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void DragAndHold(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY);
actions.Build().Perform();
});
}
public void ReleaseDrag()
{
var releaseAction = new Actions(this.Driver);
releaseAction.Release().Perform();
}
/// <summary>
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
/// </summary>
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
public void KeyDownAndDrag(Key key, int targetX, int targetY)
{
HoldShiftToDrag(key, targetX, targetY);
ReleaseAction();
ReleaseKey(key);
}
/// <summary>
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
/// </summary>
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
public void HoldShiftToDrag(Key key, int targetX, int targetY)
{
PerformAction((actions, windowElement) =>
{
KeyboardHelper.PressKey(key);
actions.MoveToElement(WindowsElement)
.ClickAndHold()
.Perform();
int dx = targetX - windowElement.Rect.X;
int dy = targetY - windowElement.Rect.Y;
int stepCount = 10;
int stepX = dx / stepCount;
int stepY = dy / stepCount;
for (int i = 0; i < stepCount; i++)
{
var stepAction = new Actions(Driver);
stepAction.MoveByOffset(stepX, stepY).Perform();
}
});
}
}
}

View File

@@ -1,131 +0,0 @@
// 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 OpenQA.Selenium.Appium.Windows;
namespace Microsoft.PowerToys.UITest
{
public class Slider : Element
{
private static readonly string ExpectedControlType = "ControlType.Slider";
/// <summary>
/// Initializes a new instance of the <see cref="Slider"/> class.
/// </summary>
public Slider()
{
this.TargetControlType = Slider.ExpectedControlType;
}
/// <summary>
/// Gets the value of a Slider (WindowsElement)
/// </summary>
/// <returns>The integer value of the slider</returns>
public int GetValue()
{
return this.Text == string.Empty ? 0 : int.Parse(this.Text);
}
/// <summary>
/// Sends a combination of keys.
/// </summary>
/// <param name="keys">The keys to send.</param>
public void SendKeys(params Key[] keys)
{
PerformAction((actions, windowElement) =>
{
KeyboardHelper.SendKeys(keys);
});
}
/// <summary>
/// Sets the value of a Slider (WindowsElement) to the specified integer value.
/// Throws an exception if the value is out of the slider's valid range.
/// </summary>
/// <param name="targetValue">The target integer value to set</param>
public void SetValue(int targetValue)
{
// Read range and current value
int min = int.Parse(this.GetAttribute("RangeValue.Minimum"));
int max = int.Parse(this.GetAttribute("RangeValue.Maximum"));
int current = int.Parse(this.Text);
// Use Assert to check if the target value is within the valid range
Assert.IsTrue(
targetValue >= min && targetValue <= max,
$"Target value {targetValue} is out of range (min: {min}, max: {max}).");
// Compute difference
int diff = targetValue - current;
if (diff == 0)
{
return;
}
string key = diff > 0 ? OpenQA.Selenium.Keys.Right : OpenQA.Selenium.Keys.Left;
int steps = Math.Abs(diff);
for (int i = 0; i < steps; i++)
{
this.SendKeys(key);
// Thread.Sleep(2);
}
// Final check
int finalValue = int.Parse(this.Text);
Assert.AreEqual(
targetValue, finalValue, $"Slider value mismatch: expected {targetValue}, but got {finalValue}.");
}
/// <summary>
/// Sets the value of a Slider (WindowsElement) to the specified integer value.
/// Throws an exception if the value is out of the slider's valid range.
/// </summary>
/// <param name="targetValue">The target integer value to set</param>
public void QuickSetValue(int targetValue)
{
// Read range and current value
int min = int.Parse(this.GetAttribute("RangeValue.Minimum"));
int max = int.Parse(this.GetAttribute("RangeValue.Maximum"));
int current = int.Parse(this.Text);
// Use Assert to check if the target value is within the valid range
Assert.IsTrue(
targetValue >= min && targetValue <= max,
$"Target value {targetValue} is out of range (min: {min}, max: {max}).");
// Compute difference
int diff = targetValue - current;
if (diff == 0)
{
return;
}
string key = diff > 0 ? OpenQA.Selenium.Keys.Right : OpenQA.Selenium.Keys.Left;
int steps = Math.Abs(diff);
int maxKeysPerSend = 50;
int fullChunks = steps / maxKeysPerSend;
int remainder = steps % maxKeysPerSend;
for (int i = 0; i < fullChunks; i++)
{
SendKeys(new string(key[0], maxKeysPerSend));
Thread.Sleep(2);
}
if (remainder > 0)
{
SendKeys(new string(key[0], remainder));
Thread.Sleep(2);
}
// Final check
int finalValue = int.Parse(this.Text);
Assert.AreEqual(
targetValue, finalValue, $"Slider value mismatch: expected {targetValue}, but got {finalValue}.");
}
}
}

View File

@@ -1,67 +0,0 @@
// 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 OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
public class Tab : Element
{
private static readonly string ExpectedControlType = "ControlType.Tab";
/// <summary>
/// Initializes a new instance of the <see cref="Tab"/> class.
/// </summary>
public Tab()
{
this.TargetControlType = Tab.ExpectedControlType;
}
/// <summary>
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
/// </summary>
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
public void KeyDownAndDrag(Key key, int targetX, int targetY)
{
HoldShiftToDrag(key, targetX, targetY);
ReleaseAction();
ReleaseKey(key);
}
/// <summary>
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
/// </summary>
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
public void HoldShiftToDrag(Key key, int targetX, int targetY)
{
PerformAction((actions, windowElement) =>
{
KeyboardHelper.PressKey(key);
actions.MoveToElement(WindowsElement)
.ClickAndHold()
.Perform();
int dx = targetX - windowElement.Rect.X;
int dy = targetY - windowElement.Rect.Y;
int stepCount = 10;
int stepX = dx / stepCount;
int stepY = dy / stepCount;
for (int i = 0; i < stepCount; i++)
{
var stepAction = new Actions(Driver);
stepAction.MoveByOffset(stepX, stepY).Perform();
}
});
}
}
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using OpenQA.Selenium;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
@@ -33,10 +35,9 @@ namespace Microsoft.PowerToys.UITest
PerformAction((actions, windowElement) =>
{
// select all text and delete it
windowElement.SendKeys(OpenQA.Selenium.Keys.Control + "a");
windowElement.SendKeys(OpenQA.Selenium.Keys.Delete);
windowElement.SendKeys(Keys.Control + "a");
windowElement.SendKeys(Keys.Delete);
});
Task.Delay(500).Wait();
}
PerformAction((actions, windowElement) =>

View File

@@ -1,36 +0,0 @@
// 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 OpenQA.Selenium.Appium.Windows;
namespace Microsoft.PowerToys.UITest
{
public class Thumb : Element
{
private static readonly string ExpectedControlType = "ControlType.Thumb";
/// <summary>
/// Initializes a new instance of the <see cref="Thumb"/> class.
/// </summary>
public Thumb()
{
this.TargetControlType = Thumb.ExpectedControlType;
}
/// <summary>
/// Drag element move offset.
/// </summary>
/// <param name="offsetX">The offsetX to move.</param>
/// <param name="offsetY">The offsetY to move.</param>
public void Drag(int offsetX, int offsetY)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
actions.Build().Perform();
});
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Microsoft.PowerToys.UITest
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var items = FindElementsWithRetry(findElementsFunc, timeoutMS);
var items = findElementsFunc();
var res = items.Select(item =>
{
var element = item as WindowsElement;
@@ -30,30 +30,17 @@ namespace Microsoft.PowerToys.UITest
return new ReadOnlyCollection<T>(res);
}
private static ReadOnlyCollection<TW> FindElementsWithRetry<TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, int timeoutMS)
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var timeout = TimeSpan.FromMilliseconds(timeoutMS);
var retryIntervalMS = TimeSpan.FromMilliseconds(500);
DateTime startTime = DateTime.Now;
while (DateTime.Now - startTime < timeout)
var items = findElementsFunc();
var res = items.Select(item =>
{
try
{
var items = findElementsFunc();
if (items.Count > 0)
{
return new ReadOnlyCollection<TW>((IList<TW>)items);
}
var element = item as WindowsElement;
return NewElement<T>(element, driver, timeoutMS);
}).Where(item => item.IsMatchingTarget()).ToList();
Task.Delay(retryIntervalMS).Wait();
}
catch (Exception)
{
}
}
return new ReadOnlyCollection<TW>(new List<TW>());
return new ReadOnlyCollection<T>(res);
}
public static T NewElement<T>(WindowsElement? element, WindowsDriver<WindowsElement>? driver, int timeoutMS)
@@ -63,6 +50,11 @@ namespace Microsoft.PowerToys.UITest
Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null.");
T newElement = new T();
if (timeoutMS > 0)
{
// Only set timeout if it is positive value
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
}
newElement.SetSession(driver);
newElement.SetWindowsElement(element);

View File

@@ -1,475 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents keyboard keys.
/// </summary>
public enum Key
{
Ctrl,
LCtrl,
RCtrl,
Alt,
Shift,
Tab,
Esc,
Enter,
Win,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Num0,
Num1,
Num2,
Num3,
Num4,
Num5,
Num6,
Num7,
Num8,
Num9,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Space,
Backspace,
Delete,
Insert,
Home,
End,
PageUp,
PageDown,
Up,
Down,
Left,
Right,
Other,
}
/// <summary>
    /// Provides methods for simulating keyboard input.
    /// </summary>
internal static class KeyboardHelper
{
[DllImport("user32.dll")]
#pragma warning disable SA1300 // Element should begin with upper-case letter
private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
#pragma warning restore SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1310 // Field names should not contain underscore
private const byte VK_LWIN = 0x5B;
private const uint KEYEVENTF_KEYDOWN = 0x0000;
private const uint KEYEVENTF_KEYUP = 0x0002;
#pragma warning restore SA1310 // Field names should not contain underscore
/// <summary>
/// Sends a combination of keys.
/// </summary>
/// <param name="keys">The keys to send.</param>
public static void SendKeys(params Key[] keys)
{
string keysToSend = string.Join(string.Empty, keys.Select(TranslateKey));
SendWinKeyCombination(keysToSend);
}
public static void PressKey(Key key)
{
PressVirtualKey(TranslateKeyHex(key));
}
public static void ReleaseKey(Key key)
{
ReleaseVirtualKey(TranslateKeyHex(key));
}
public static void SendKey(Key key)
{
PressVirtualKey(TranslateKeyHex(key));
ReleaseVirtualKey(TranslateKeyHex(key));
}
/// <summary>
        /// Translates a key to its corresponding SendKeys representation.
        /// </summary>
        /// <param name="key">The key to translate.</param>
        /// <returns>The SendKeys representation of the key.</returns>
private static string TranslateKey(Key key)
{
switch (key)
{
case Key.Ctrl:
return "^";
case Key.LCtrl:
return "^";
case Key.RCtrl:
return "^";
case Key.Alt:
return "%";
case Key.Shift:
return "+";
case Key.Tab:
return "{TAB}";
case Key.Esc:
return "{ESC}";
case Key.Enter:
return "{ENTER}";
case Key.Win:
return "{WIN}";
case Key.Space:
return " ";
case Key.Backspace:
return "{BACKSPACE}";
case Key.Delete:
return "{DELETE}";
case Key.Insert:
return "{INSERT}";
case Key.Home:
return "{HOME}";
case Key.End:
return "{END}";
case Key.PageUp:
return "{PGUP}";
case Key.PageDown:
return "{PGDN}";
case Key.Up:
return "{UP}";
case Key.Down:
return "{DOWN}";
case Key.Left:
return "{LEFT}";
case Key.Right:
return "{RIGHT}";
case Key.F1:
return "{F1}";
case Key.F2:
return "{F2}";
case Key.F3:
return "{F3}";
case Key.F4:
return "{F4}";
case Key.F5:
return "{F5}";
case Key.F6:
return "{F6}";
case Key.F7:
return "{F7}";
case Key.F8:
return "{F8}";
case Key.F9:
return "{F9}";
case Key.F10:
return "{F10}";
case Key.F11:
return "{F11}";
case Key.F12:
return "{F12}";
case Key.A:
return "a";
case Key.B:
return "b";
case Key.C:
return "c";
case Key.D:
return "d";
case Key.E:
return "e";
case Key.F:
return "f";
case Key.G:
return "g";
case Key.H:
return "h";
case Key.I:
return "i";
case Key.J:
return "j";
case Key.K:
return "k";
case Key.L:
return "l";
case Key.M:
return "m";
case Key.N:
return "n";
case Key.O:
return "o";
case Key.P:
return "p";
case Key.Q:
return "q";
case Key.R:
return "r";
case Key.S:
return "s";
case Key.T:
return "t";
case Key.U:
return "u";
case Key.V:
return "v";
case Key.W:
return "w";
case Key.X:
return "x";
case Key.Y:
return "y";
case Key.Z:
return "z";
case Key.Num0:
return "0";
case Key.Num1:
return "1";
case Key.Num2:
return "2";
case Key.Num3:
return "3";
case Key.Num4:
return "4";
case Key.Num5:
return "5";
case Key.Num6:
return "6";
case Key.Num7:
return "7";
case Key.Num8:
return "8";
case Key.Num9:
return "9";
default:
return string.Empty;
}
}
/// <summary>
/// map the virtual key codes to the corresponding keys.
/// </summary>
private static byte TranslateKeyHex(Key key)
{
switch (key)
{
case Key.Win:
return 0x5B; // Windows Key - 0x5B in hex
case Key.Ctrl:
return 0x11; // Ctrl Key - 0x11 in hex
case Key.Alt:
return 0x12; // Alt Key - 0x12 in hex
case Key.Shift:
return 0x10; // Shift Key - 0x10 in hex
case Key.LCtrl:
return 0xA2; // Left Ctrl Key - 0xA2 in hex
case Key.RCtrl:
return 0xA3; // Right Ctrl Key - 0xA3 in hex
case Key.A:
return 0x41; // A Key - 0x41 in hex
case Key.B:
return 0x42; // B Key - 0x42 in hex
case Key.C:
return 0x43; // C Key - 0x43 in hex
case Key.D:
return 0x44; // D Key - 0x44 in hex
case Key.E:
return 0x45; // E Key - 0x45 in hex
case Key.F:
return 0x46; // F Key - 0x46 in hex
case Key.G:
return 0x47; // G Key - 0x47 in hex
case Key.H:
return 0x48; // H Key - 0x48 in hex
case Key.I:
return 0x49; // I Key - 0x49 in hex
case Key.J:
return 0x4A; // J Key - 0x4A in hex
case Key.K:
return 0x4B; // K Key - 0x4B in hex
case Key.L:
return 0x4C; // L Key - 0x4C in hex
case Key.M:
return 0x4D; // M Key - 0x4D in hex
case Key.N:
return 0x4E; // N Key - 0x4E in hex
case Key.O:
return 0x4F; // O Key - 0x4F in hex
case Key.P:
return 0x50; // P Key - 0x50 in hex
case Key.Q:
return 0x51; // Q Key - 0x51 in hex
case Key.R:
return 0x52; // R Key - 0x52 in hex
case Key.S:
return 0x53; // S Key - 0x53 in hex
case Key.T:
return 0x54; // T Key - 0x54 in hex
case Key.U:
return 0x55; // U Key - 0x55 in hex
case Key.V:
return 0x56; // V Key - 0x56 in hex
case Key.W:
return 0x57; // W Key - 0x57 in hex
case Key.X:
return 0x58; // X Key - 0x58 in hex
case Key.Y:
return 0x59; // Y Key - 0x59 in hex
case Key.Z:
return 0x5A; // Z Key - 0x5A in hex
case Key.Num0:
return 0x30; // 0 Key - 0x30 in hex
case Key.Num1:
return 0x31; // 1 Key - 0x31 in hex
case Key.Num2:
return 0x32; // 2 Key - 0x32 in hex
case Key.Num3:
return 0x33; // 3 Key - 0x33 in hex
case Key.Num4:
return 0x34; // 4 Key - 0x34 in hex
case Key.Num5:
return 0x35; // 5 Key - 0x35 in hex
case Key.Num6:
return 0x36; // 6 Key - 0x36 in hex
case Key.Num7:
return 0x37; // 7 Key - 0x37 in hex
case Key.Num8:
return 0x38; // 8 Key - 0x38 in hex
case Key.Num9:
return 0x39; // 9 Key - 0x39 in hex
case Key.F1:
return 0x70; // F1 Key - 0x70 in hex
case Key.F2:
return 0x71; // F2 Key - 0x71 in hex
case Key.F3:
return 0x72; // F3 Key - 0x72 in hex
case Key.F4:
return 0x73; // F4 Key - 0x73 in hex
case Key.F5:
return 0x74; // F5 Key - 0x74 in hex
case Key.F6:
return 0x75; // F6 Key - 0x75 in hex
case Key.F7:
return 0x76; // F7 Key - 0x76 in hex
case Key.F8:
return 0x77; // F8 Key - 0x77 in hex
case Key.F9:
return 0x78; // F9 Key - 0x78 in hex
case Key.F10:
return 0x79; // F10 Key - 0x79 in hex
case Key.F11:
return 0x7A; // F11 Key - 0x7A in hex
case Key.F12:
return 0x7B; // F12 Key - 0x7B in hex
case Key.Up:
return 0x26; // Up Arrow Key - 0x26 in hex
case Key.Down:
return 0x28; // Down Arrow Key - 0x28 in hex
case Key.Left:
return 0x25; // Left Arrow Key - 0x25 in hex
case Key.Right:
return 0x27; // Right Arrow Key - 0x27 in hex
case Key.Home:
return 0x24; // Home Key - 0x24 in hex
case Key.End:
return 0x23; // End Key - 0x23 in hex
case Key.PageUp:
return 0x21; // Page Up Key - 0x21 in hex
case Key.PageDown:
return 0x22; // Page Down Key - 0x22 in hex
case Key.Space:
return 0x20; // Space Key - 0x20 in hex
case Key.Enter:
return 0x0D; // Enter Key - 0x0D in hex
case Key.Backspace:
return 0x08; // Backspace Key - 0x08 in hex
case Key.Tab:
return 0x09; // Tab Key - 0x09 in hex
case Key.Esc:
return 0x1B; // Escape Key - 0x1B in hex
case Key.Insert:
return 0x2D; // Insert Key - 0x2D in hex
case Key.Delete:
return 0x2E; // Delete Key - 0x2E in hex
default:
throw new ArgumentException($"Key {key} is not supported, Please add your key at TranslateKeyHex for translation to hex.");
}
}
/// <summary>
/// Sends a combination of keys, including the Windows key, to the system.
/// </summary>
/// <param name="keys">The keys to send.</param>
private static void SendWinKeyCombination(string keys)
{
bool winKeyDown = false;
if (keys.Contains("{WIN}"))
{
keybd_event(VK_LWIN, 0, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
winKeyDown = true;
keys = keys.Replace("{WIN}", string.Empty); // Remove {WIN} from the string
        }
System.Windows.Forms.SendKeys.SendWait(keys);
        // Release Windows key
if (winKeyDown)
{
keybd_event(VK_LWIN, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
}
/// <summary>
/// Just press the key.(no release)
/// </summary>
private static void PressVirtualKey(byte key)
{
keybd_event(key, 0, KEYEVENTF_KEYDOWN, UIntPtr.Zero);
}
/// <summary>
/// Release only the button (if pressed first)
/// </summary>
private static void ReleaseVirtualKey(byte key)
{
keybd_event(key, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
}
}
}

View File

@@ -29,49 +29,6 @@ namespace Microsoft.PowerToys.UITest
PowerToysSettings,
FancyZone,
Hosts,
Runner,
Workspaces,
}
/// <summary>
/// Represents the window size for the UI test.
/// </summary>
public enum WindowSize
{
/// <summary>
/// Unspecified window size, won't make any size change
/// </summary>
UnSpecified,
/// <summary>
/// Small window size, 640 * 480
/// </summary>
Small,
/// <summary>
/// Small window size, 480 * 640
/// </summary>
Small_Vertical,
/// <summary>
/// Medium window size, 1024 * 768
/// </summary>
Medium,
/// <summary>
/// Medium window size, 768 * 1024
/// </summary>
Medium_Vertical,
/// <summary>
/// Large window size, 1920 * 1080
/// </summary>
Large,
/// <summary>
/// Large window size, 1080 * 1920
/// </summary>
Large_Vertical,
}
internal class ModuleConfigData
@@ -95,8 +52,6 @@ namespace Microsoft.PowerToys.UITest
[PowerToysModule.PowerToysSettings] = "PowerToys Settings",
[PowerToysModule.FancyZone] = "FancyZones Layout",
[PowerToysModule.Hosts] = "Hosts File Editor",
[PowerToysModule.Runner] = "PowerToys",
[PowerToysModule.Workspaces] = "Workspaces Editor",
};
// Exe start path for the module if it exists.
@@ -105,8 +60,6 @@ namespace Microsoft.PowerToys.UITest
[PowerToysModule.PowerToysSettings] = @"\..\..\..\WinUI3Apps\PowerToys.Settings.exe",
[PowerToysModule.FancyZone] = @"\..\..\..\PowerToys.FancyZonesEditor.exe",
[PowerToysModule.Hosts] = @"\..\..\..\WinUI3Apps\PowerToys.Hosts.exe",
[PowerToysModule.Runner] = @"\..\..\..\PowerToys.exe",
[PowerToysModule.Workspaces] = @"\..\..\..\PowerToys.WorkspacesEditor.exe",
};
}

View File

@@ -1,37 +0,0 @@
// 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.Collections.Generic;
namespace Microsoft.PowerToys.UITest
{
public class MonitorInfoData
{
public MonitorInfoData()
{
}
public struct MonitorInfoDataWrapper
{
public string DeviceName { get; set; }
public string DeviceString { get; set; }
public string DeviceID { get; set; }
public string DeviceKey { get; set; }
public int PelsWidth { get; set; }
public int PelsHeight { get; set; }
public int DisplayFrequency { get; set; }
}
public struct ParamsWrapper
{
public List<MonitorInfoDataWrapper> Monitors { get; set; }
}
}
}

View File

@@ -1,217 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
public enum MouseActionType
{
LeftClick,
RightClick,
MiddleClick,
LeftDoubleClick,
RightDoubleClick,
LeftDown,
LeftUp,
RightDown,
RightUp,
MiddleDown,
MiddleUp,
ScrollUp,
ScrollDown,
}
internal static class MouseHelper
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[Flags]
internal enum MouseEvent
{
LeftDown = 0x0002,
LeftUp = 0x0004,
RightDown = 0x0008,
RightUp = 0x0010,
MiddleDown = 0x0020,
MiddleUp = 0x0040,
Wheel = 0x0800,
}
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
#pragma warning disable SA1300 // Element should begin with upper-case letter
private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);
#pragma warning restore SA1300 // Element should begin with upper-case letter
        /// <summary>
        /// Gets the current position of the mouse cursor as a tuple.
        /// </summary>
        /// <returns>A tuple containing the X and Y coordinates of the cursor.</returns>
public static Tuple<int, int> GetMousePosition()
{
GetCursorPos(out POINT point);
return Tuple.Create(point.X, point.Y);
}
/// <summary>
    /// Moves the mouse cursor to the specified screen coordinates.
    /// </summary>
    /// <param name="x">The new x-coordinate of the cursor.</param>
    /// <param name="y">The new y-coordinate of the cursor.</param
public static void MoveMouseTo(int x, int y)
{
SetCursorPos(x, y);
}
/// <summary>
/// The delay in milliseconds between mouse down and up events to simulate a click.
/// </summary>
private const int ClickDelay = 100;
/// <summary>
/// The amount of scroll units to simulate a single mouse wheel tick.
/// </summary>
private const int ScrollAmount = 120;
/// <summary>
/// Simulates a left mouse click (press and release).
/// </summary>
public static void LeftClick()
{
LeftDown();
Thread.Sleep(ClickDelay);
LeftUp();
}
/// <summary>
/// Simulates a right mouse click (press and release).
/// </summary>
public static void RightClick()
{
RightDown();
Thread.Sleep(ClickDelay);
RightUp();
}
/// <summary>
/// Simulates a middle mouse click (press and release).
/// </summary>
public static void MiddleClick()
{
MiddleDown();
Thread.Sleep(ClickDelay);
MiddleUp();
}
/// <summary>
/// Simulates a left mouse double-click.
/// </summary>
public static void LeftDoubleClick()
{
LeftClick();
Thread.Sleep(ClickDelay);
LeftClick();
}
/// <summary>
/// Simulates a right mouse double-click.
/// </summary>
public static void RightDoubleClick()
{
RightClick();
Thread.Sleep(ClickDelay);
RightClick();
}
/// <summary>
/// Simulates pressing the left mouse button down.
/// </summary>
public static void LeftDown()
{
mouse_event((uint)MouseEvent.LeftDown, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates pressing the right mouse button down.
/// </summary>
public static void RightDown()
{
mouse_event((uint)MouseEvent.RightDown, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates pressing the middle mouse button down.
/// </summary>
public static void MiddleDown()
{
mouse_event((uint)MouseEvent.MiddleDown, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates releasing the left mouse button.
/// </summary>
public static void LeftUp()
{
mouse_event((uint)MouseEvent.LeftUp, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates releasing the right mouse button.
/// </summary>
public static void RightUp()
{
mouse_event((uint)MouseEvent.RightUp, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates releasing the middle mouse button.
/// </summary>
public static void MiddleUp()
{
mouse_event((uint)MouseEvent.MiddleUp, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// Simulates a mouse scroll wheel action by a specified amount.
/// Positive values scroll up, negative values scroll down.
/// </summary>
/// <param name="amount">The scroll amount. Typically 120 or -120 per tick.</param>
public static void ScrollWheel(int amount)
{
mouse_event((uint)MouseEvent.Wheel, 0, 0, (uint)amount, UIntPtr.Zero);
}
/// <summary>
/// Simulates scrolling the mouse wheel up by one tick.
/// </summary>
public static void ScrollUp()
{
ScrollWheel(ScrollAmount);
}
/// <summary>
/// Simulates scrolling the mouse wheel down by one tick.
/// </summary>
public static void ScrollDown()
{
ScrollWheel(-ScrollAmount);
}
}
}

View File

@@ -1,133 +0,0 @@
// 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.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Provides methods for capturing the screen with the mouse cursor.
/// </summary>
internal static class ScreenCapture
{
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool GetCursorInfo(out CURSORINFO pci);
[DllImport("user32.dll")]
private static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int cx, int cy, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);
/// <summary>
/// Represents a point with X and Y coordinates.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
/// <summary>
/// Contains information about the cursor.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct CURSORINFO
{
/// <summary>
/// Gets or sets the size of the structure.
/// </summary>
public int CbSize;
/// <summary>
/// Gets or sets the cursor state.
/// </summary>
public int Flags;
/// <summary>
/// Gets or sets the handle to the cursor.
/// </summary>
public IntPtr HCursor;
/// <summary>
/// Gets or sets the screen position of the cursor.
/// </summary>
public POINT PTScreenPos;
}
private const int CURSORSHOWING = 0x00000001;
private const int DESKTOPHORZRES = 118;
private const int DESKTOPVERTRES = 117;
private const int DINORMAL = 0x0003;
/// <summary>
/// Captures the screen with the mouse cursor and saves it to the specified file path.
/// </summary>
/// <param name="filePath">The file path to save the captured image.</param>
private static void CaptureScreenWithMouse(string filePath)
{
IntPtr hdc = GetDC(IntPtr.Zero);
int screenWidth = GetDeviceCaps(hdc, DESKTOPHORZRES);
int screenHeight = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
Rectangle bounds = new Rectangle(0, 0, screenWidth, screenHeight);
using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height))
{
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap))
{
g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
CURSORINFO cursorInfo;
cursorInfo.CbSize = Marshal.SizeOf<CURSORINFO>();
if (GetCursorInfo(out cursorInfo) && cursorInfo.Flags == CURSORSHOWING)
{
using (System.Drawing.Graphics gIcon = System.Drawing.Graphics.FromImage(bitmap))
{
IntPtr hdcDest = gIcon.GetHdc();
DrawIconEx(hdcDest, cursorInfo.PTScreenPos.X, cursorInfo.PTScreenPos.Y, cursorInfo.HCursor, 0, 0, 0, IntPtr.Zero, DINORMAL);
gIcon.ReleaseHdc(hdcDest);
}
}
}
bitmap.Save(filePath, ImageFormat.Png);
}
}
/// <summary>
/// Captures a screenshot and saves it to the specified directory.
/// </summary>
/// <param name="directory">The directory to save the screenshot.</param>
private static void CaptureScreenshot(string directory)
{
string filePath = Path.Combine(directory, $"screenshot_{DateTime.Now:yyyyMMdd_HHmmssfff}.png");
CaptureScreenWithMouse(filePath);
}
/// <summary>
/// Timer callback method to capture a screenshot.
/// </summary>
/// <param name="state">The state object passed to the callback method.</param>
public static void TimerCallback(object? state)
{
string directory = (string)state!;
CaptureScreenshot(directory);
}
}
}

View File

@@ -1,17 +1,14 @@
// Copyright (c) Microsoft Corporation
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using static Microsoft.PowerToys.UITest.WindowHelper;
namespace Microsoft.PowerToys.UITest
{
@@ -20,70 +17,35 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public class Session
{
public WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement> WindowsDriver { get; set; }
private List<IntPtr> windowHandlers = new List<IntPtr>();
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);
private Window? MainWindow { get; set; }
/// <summary>
/// Gets Main Window Handler
/// </summary>
public IntPtr MainWindowHandler { get; private set; }
/// <summary>
/// Gets Init Scope
/// </summary>
public PowerToysModule InitScope { get; private set; }
/// <summary>
/// Gets the RunAsAdmin flag.
/// If true, the session is running as admin.
/// If false, the session is not running as admin.
/// If null, no information is available.
/// </summary>
public bool? IsElevated { get; private set; }
public Session(WindowsDriver<WindowsElement> pRoot, WindowsDriver<WindowsElement> pDriver, PowerToysModule scope, WindowSize size)
public Session(WindowsDriver<WindowsElement> root, WindowsDriver<WindowsElement> windowsDriver)
{
this.MainWindowHandler = IntPtr.Zero;
this.Root = pRoot;
this.WindowsDriver = pDriver;
this.InitScope = scope;
if (size != WindowSize.UnSpecified)
{
// Attach to the scope & reset MainWindowHandler
this.Attach(scope, size);
}
this.Root = root;
this.WindowsDriver = windowsDriver;
}
/// <summary>
/// Cleans up the Session Exe.
/// </summary>
public void Cleanup()
{
windowHandlers.Clear();
}
/// <summary>
/// Finds an Element or its derived class by selector.
/// Finds an element by selector.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public T Find<T>(By by, int timeoutMS = 5000, bool global = false)
public T Find<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
// leverage findAll to filter out mismatched elements
var collection = this.FindAll<T>(by, timeoutMS, global);
var collection = this.FindAll<T>(by, timeoutMS);
Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
return collection[0];
}
@@ -93,159 +55,62 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public T Find<T>(string name, int timeoutMS = 5000, bool global = false)
public T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Find<T>(By.Name(name), timeoutMS, global);
return this.Find<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Shortcut for this.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public Element Find(By by, int timeoutMS = 5000, bool global = false)
public Element Find(By by, int timeoutMS = 3000)
{
return this.Find<Element>(by, timeoutMS, global);
return this.Find<Element>(by, timeoutMS);
}
/// <summary>
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public Element Find(string name, int timeoutMS = 5000, bool global = false)
public Element Find(string name, int timeoutMS = 3000)
{
return this.Find<Element>(By.Name(name), timeoutMS, global);
return this.Find<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Has only one Element or its derived class by selector.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne<T>(By by, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.FindAll<T>(by, timeoutMS, global).Count == 1;
}
/// <summary>
/// Shortcut for this.HasOne<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne(By by, int timeoutMS = 5000, bool global = false)
{
return this.HasOne<Element>(by, timeoutMS, global);
}
/// <summary>
/// Shortcut for this.HasOne<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne<T>(string name, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.HasOne<T>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Shortcut for this.HasOne<Element>(name, timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne(string name, int timeoutMS = 5000, bool global = false)
{
return this.HasOne<Element>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Has one or more Element or its derived class by selector.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has<T>(By by, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.FindAll<T>(by, timeoutMS, global).Count >= 1;
}
/// <summary>
/// Shortcut for this.Has<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has(By by, int timeoutMS = 5000, bool global = false)
{
return this.Has<Element>(by, timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Has<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has<T>(string name, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.Has<T>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Has<Element>(name, timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has(string name, int timeoutMS = 5000, bool global = false)
{
return this.Has<Element>(name, timeoutMS, global);
}
/// <summary>
/// Finds all Element or its derived class by selector.
/// Finds all elements by selector.
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000, bool global = false)
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
var driver = global ? this.Root : this.WindowsDriver;
Assert.IsNotNull(driver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
var foundElements = FindHelper.FindAll<T, WindowsElement>(
() =>
{
if (by.GetIsAccessibilityId())
{
var elements = driver.FindElementsByAccessibilityId(by.GetAccessibilityId());
var elements = this.WindowsDriver.FindElementsByAccessibilityId(by.GetAccessibilityId());
return elements;
}
else
{
var elements = driver.FindElements(by.ToSeleniumBy());
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
return elements;
}
},
driver,
this.WindowsDriver,
timeoutMS);
return foundElements ?? new ReadOnlyCollection<T>([]);
@@ -257,12 +122,12 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="name">The name to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000, bool global = false)
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.FindAll<T>(By.Name(name), timeoutMS, global);
return this.FindAll<T>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -270,11 +135,11 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000, bool global = false)
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.FindAll<Element>(by, timeoutMS, global);
return this.FindAll<Element>(by, timeoutMS);
}
/// <summary>
@@ -282,186 +147,55 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000, bool global = false)
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.FindAll<Element>(By.Name(name), timeoutMS, global);
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Close the main window.
/// Keyboard Action key.
/// </summary>
public void CloseMainWindow()
/// <param name="key1">The Keys1 to click.</param>
/// <param name="key2">The Keys2 to click.</param>
/// <param name="key3">The Keys3 to click.</param>
/// <param name="key4">The Keys4 to click.</param>
public void KeyboardAction(string key1, string key2 = "", string key3 = "", string key4 = "")
{
if (MainWindow != null)
PerformAction((actions, windowElement) =>
{
MainWindow.Close();
MainWindow = null;
}
}
/// <summary>
/// Sends a combination of keys.
/// </summary>
/// <param name="keys">The keys to send.</param>
public void SendKeys(params Key[] keys)
{
PerformAction(() =>
{
KeyboardHelper.SendKeys(keys);
});
}
/// <summary>
/// release the key (after the hold key and drag is completed.)
/// </summary>
/// <param name="key">The key release.</param>
public void PressKey(Key key)
{
PerformAction(() =>
{
KeyboardHelper.PressKey(key);
});
}
/// <summary>
/// press and hold the specified key.
/// </summary>
/// <param name="key">The key to press and hold .</param>
public void ReleaseKey(Key key)
{
PerformAction(() =>
{
KeyboardHelper.ReleaseKey(key);
});
}
/// <summary>
/// press and hold the specified key.
/// </summary>
/// <param name="key">The key to press and release .</param>
public void SendKey(Key key, int msPreAction = 500, int msPostAction = 500)
{
PerformAction(
() =>
if (string.IsNullOrEmpty(key2))
{
KeyboardHelper.SendKey(key);
},
msPreAction,
msPostAction);
}
/// <summary>
/// Sends a sequence of keys.
/// </summary>
/// <param name="keys">An array of keys to send.</param>
public void SendKeySequence(params Key[] keys)
{
PerformAction(() =>
{
foreach (var key in keys)
{
KeyboardHelper.SendKeys(key);
actions.SendKeys(key1);
}
else if (string.IsNullOrEmpty(key3))
{
actions.SendKeys(key1).SendKeys(key2);
}
else if (string.IsNullOrEmpty(key4))
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3);
}
else
{
actions.SendKeys(key1).SendKeys(key2).SendKeys(key3).SendKeys(key4);
}
actions.Release();
actions.Build().Perform();
});
}
        /// <summary>
        /// Gets the current position of the mouse cursor as a tuple.
        /// </summary>
        /// <returns>A tuple containing the X and Y coordinates of the cursor.</returns>
public Tuple<int, int> GetMousePosition()
{
return MouseHelper.GetMousePosition();
}
/// <summary>
    /// Moves the mouse cursor to the specified screen coordinates.
    /// </summary>
    /// <param name="x">The new x-coordinate of the cursor.</param>
    /// <param name="y">The new y-coordinate of the cursor.</param
public void MoveMouseTo(int x, int y, int msPreAction = 500, int msPostAction = 500)
{
PerformAction(
() =>
{
MouseHelper.MoveMouseTo(x, y);
},
msPreAction,
msPostAction);
}
/// <summary>
/// Performs a mouse action based on the specified action type.
/// </summary>
/// <param name="action">The mouse action to perform.</param>
/// <param name="msPreAction">Pre-action delay in milliseconds.</param>
/// <param name="msPostAction">Post-action delay in milliseconds.</param>
public void PerformMouseAction(MouseActionType action, int msPreAction = 500, int msPostAction = 500)
{
PerformAction(
() =>
{
switch (action)
{
case MouseActionType.LeftClick:
MouseHelper.LeftClick();
break;
case MouseActionType.RightClick:
MouseHelper.RightClick();
break;
case MouseActionType.MiddleClick:
MouseHelper.MiddleClick();
break;
case MouseActionType.LeftDoubleClick:
MouseHelper.LeftDoubleClick();
break;
case MouseActionType.RightDoubleClick:
MouseHelper.RightDoubleClick();
break;
case MouseActionType.LeftDown:
MouseHelper.LeftDown();
break;
case MouseActionType.LeftUp:
MouseHelper.LeftUp();
break;
case MouseActionType.RightDown:
MouseHelper.RightDown();
break;
case MouseActionType.RightUp:
MouseHelper.RightUp();
break;
case MouseActionType.MiddleDown:
MouseHelper.MiddleDown();
break;
case MouseActionType.MiddleUp:
MouseHelper.MiddleUp();
break;
case MouseActionType.ScrollUp:
MouseHelper.ScrollUp();
break;
case MouseActionType.ScrollDown:
MouseHelper.ScrollDown();
break;
default:
throw new ArgumentException("Unsupported mouse action.", nameof(action));
}
},
msPreAction,
msPostAction);
}
/// <summary>
/// Attaches to an existing PowerToys module.
/// </summary>
/// <param name="module">The PowerToys module to attach to.</param>
/// <param name="size">The window size to set. Default is no change to window size</param>
/// <returns>The attached session.</returns>
public Session Attach(PowerToysModule module, WindowSize size = WindowSize.UnSpecified)
public Session Attach(PowerToysModule module)
{
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
return this.Attach(windowName, size);
return this.Attach(windowName);
}
/// <summary>
@@ -469,148 +203,35 @@ namespace Microsoft.PowerToys.UITest
/// The session should be attached when a new app is started.
/// </summary>
/// <param name="windowName">The window name to attach to.</param>
/// <param name="size">The window size to set. Default is no change to window size</param>
/// <returns>The attached session.</returns>
public Session Attach(string windowName, WindowSize size = WindowSize.UnSpecified)
public Session Attach(string windowName)
{
this.IsElevated = null;
this.MainWindowHandler = IntPtr.Zero;
if (this.Root != null)
{
// search window handler by window title (admin and non-admin titles)
var timeout = TimeSpan.FromMinutes(2);
var retryInterval = TimeSpan.FromSeconds(5);
DateTime startTime = DateTime.Now;
List<(IntPtr HWnd, string Title)>? matchingWindows = null;
while (DateTime.Now - startTime < timeout)
{
matchingWindows = WindowHelper.ApiHelper.FindDesktopWindowHandler(
new[] { windowName, WindowHelper.AdministratorPrefix + windowName });
if (matchingWindows.Count > 0 && matchingWindows[0].HWnd != IntPtr.Zero)
{
break;
}
Task.Delay(retryInterval).Wait();
}
if (matchingWindows == null || matchingWindows.Count == 0 || matchingWindows[0].HWnd == IntPtr.Zero)
{
Assert.Fail($"Failed to attach. Window '{windowName}' not found after {timeout.TotalSeconds} seconds.");
}
// pick one from matching windows
this.MainWindowHandler = matchingWindows[0].HWnd;
this.IsElevated = matchingWindows[0].Title.StartsWith(WindowHelper.AdministratorPrefix);
ApiHelper.SetForegroundWindow(this.MainWindowHandler);
var hexWindowHandle = this.MainWindowHandler.ToInt64().ToString("x");
var window = this.Root.FindElementByName(windowName);
Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found");
var windowHandle = new nint(int.Parse(window.GetAttribute("NativeWindowHandle")));
SetForegroundWindow(windowHandle);
var hexWindowHandle = windowHandle.ToString("x");
var appCapabilities = new AppiumOptions();
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
this.WindowsDriver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);
Assert.IsNotNull(this.WindowsDriver, "Attach WindowsDriver is null");
this.windowHandlers.Add(this.MainWindowHandler);
if (size != WindowSize.UnSpecified)
{
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
}
// Set MainWindow
MainWindow = Find<Window>(matchingWindows[0].Title);
// Set implicit timeout to make element search retry every 500 ms
this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);
}
else
{
Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null");
}
Task.Delay(3000).Wait();
return this;
}
/// <summary>
/// Sets the main window size.
/// </summary>
/// <param name="size">WindowSize enum</param>
public void SetMainWindowSize(WindowSize size)
{
if (this.MainWindowHandler == IntPtr.Zero)
{
// Attach to the scope & reset MainWindowHandler
this.Attach(this.InitScope);
}
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
}
/// <summary>
/// Gets the main window center coordinates.
/// </summary>
/// <returns>(x, y)</returns>
public (int CenterX, int CenterY) GetMainWindowCenter()
{
return WindowHelper.GetWindowCenter(this.MainWindowHandler);
}
/// <summary>
/// Gets the main window center coordinates.
/// </summary>
/// <returns>(int Left, int Top, int Right, int Bottom)</returns>
public (int Left, int Top, int Right, int Bottom) GetMainWindowRect()
{
return WindowHelper.GetWindowRect(this.MainWindowHandler);
}
/// <summary>
/// Launches the specified executable with optional arguments and simulates a delay before and after execution.
/// </summary>
/// <param name="executablePath">The full path to the executable to launch.</param>
/// <param name="arguments">Optional command-line arguments to pass to the executable.</param>
/// <param name="msPreAction">The number of milliseconds to wait before launching the executable. Default is 0 ms.</param>
/// <param name="msPostAction">The number of milliseconds to wait after launching the executable. Default is 2000 ms.</param>
public void StartExe(string executablePath, string arguments = "", int msPreAction = 0, int msPostAction = 2000)
{
PerformAction(
() =>
{
StartExeInternal(executablePath, arguments);
},
msPreAction,
msPostAction);
}
private void StartExeInternal(string executablePath, string arguments = "")
{
var processInfo = new ProcessStartInfo
{
FileName = executablePath,
Arguments = arguments,
UseShellExecute = true,
};
Process.Start(processInfo);
}
/// <summary>
/// Terminates all running processes that match the specified process name.
/// Waits for each process to exit after sending the kill signal.
/// </summary>
/// <param name="processName">The name of the process to terminate (without extension, e.g., "notepad").</param>
public void KillAllProcessesByName(string processName)
{
foreach (var process in Process.GetProcessesByName(processName))
{
process.Kill();
process.WaitForExit();
}
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
@@ -633,26 +254,5 @@ namespace Microsoft.PowerToys.UITest
Task.Delay(msPostAction).Wait();
}
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
/// <param name="action">The action to perform on the element.</param>
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 500 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
protected void PerformAction(Action action, int msPreAction = 500, int msPostAction = 500)
{
if (msPreAction > 0)
{
Task.Delay(msPreAction).Wait();
}
action();
if (msPostAction > 0)
{
Task.Delay(msPostAction).Wait();
}
}
}
}

View File

@@ -15,58 +15,39 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Nested class for test initialization.
/// </summary>
public class SessionHelper
internal class SessionHelper
{
// Default session path is PowerToys settings dashboard
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
private readonly string runnerPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.Runner);
private string? locationPath;
private static WindowsDriver<WindowsElement>? root;
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement>? Driver { get; set; }
private static Process? appDriver;
private Process? runner;
private PowerToysModule scope;
private Process? appDriver;
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper(PowerToysModule scope)
{
this.scope = scope;
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
this.locationPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
CheckWinAppDriverAndRoot();
var runnerProcessInfo = new ProcessStartInfo
var winAppDriverProcessInfo = new ProcessStartInfo
{
FileName = locationPath + this.runnerPath,
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
Verb = "runas",
};
if (scope == PowerToysModule.PowerToysSettings)
{
this.ExitExe(runnerProcessInfo.FileName);
this.runner = Process.Start(runnerProcessInfo);
}
}
this.appDriver = Process.Start(winAppDriverProcessInfo);
/// <summary>
/// Initializes WinAppDriver And Root.
/// </summary>
public void CheckWinAppDriverAndRoot()
{
if (SessionHelper.root == null || SessionHelper.appDriver?.SessionId == null || SessionHelper.appDriver == null || SessionHelper.appDriver.HasExited)
{
this.StartWindowsAppDriverApp();
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
SessionHelper.root = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
}
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
this.Root = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
// Set default timeout to 5 seconds
this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
@@ -75,8 +56,7 @@ namespace Microsoft.PowerToys.UITest
/// <param name="scope">The PowerToys module to start.</param>
public SessionHelper Init()
{
this.ExitExe(this.locationPath + this.sessionPath);
this.StartExe(this.locationPath + this.sessionPath);
this.StartExe(locationPath + this.sessionPath);
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
@@ -91,11 +71,8 @@ namespace Microsoft.PowerToys.UITest
ExitScopeExe();
try
{
if (this.scope == PowerToysModule.PowerToysSettings)
{
runner?.Kill();
runner?.WaitForExit(); // Optional: Wait for the process to exit
}
appDriver?.Kill();
appDriver?.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception ex)
{
@@ -104,30 +81,6 @@ namespace Microsoft.PowerToys.UITest
}
}
/// <summary>
/// Exit a exe.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void ExitExe(string appPath)
{
// Exit Exe
string exeName = Path.GetFileNameWithoutExtension(appPath);
Process[] processes = Process.GetProcessesByName(exeName);
foreach (Process process in processes)
{
try
{
process.Kill();
process.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception ex)
{
Assert.Fail($"Failed to terminate process {process.ProcessName} (ID: {process.Id}): {ex.Message}");
}
}
}
/// <summary>
/// Starts a new exe and takes control of it.
/// </summary>
@@ -136,36 +89,34 @@ namespace Microsoft.PowerToys.UITest
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
this.Driver = NewWindowsDriver(opts);
Console.WriteLine($"appPath: {appPath}");
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Starts a new exe and takes control of it.
/// Exit a exe.
/// </summary>
/// <param name="info">The path to the application executable.</param>
private WindowsDriver<WindowsElement> NewWindowsDriver(AppiumOptions info)
/// <param name="path">The path to the application executable.</param>
public void ExitExe(string path)
{
// Create driver with retry
var timeout = TimeSpan.FromMinutes(2);
var retryInterval = TimeSpan.FromSeconds(5);
DateTime startTime = DateTime.Now;
// Exit Exe
string exeName = Path.GetFileNameWithoutExtension(path);
while (true)
// PowerToys.FancyZonesEditor
Process[] processes = Process.GetProcessesByName(exeName);
foreach (Process process in processes)
{
try
{
var res = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), info);
return res;
process.Kill();
process.WaitForExit(); // Optional: Wait for the process to exit
}
catch (Exception)
catch (Exception ex)
{
if (DateTime.Now - startTime > timeout)
{
throw;
}
Task.Delay(retryInterval).Wait();
CheckWinAppDriverAndRoot();
Assert.Fail($"Failed to terminate process {process.ProcessName} (ID: {process.Id}): {ex.Message}");
}
}
}
@@ -183,31 +134,16 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public void RestartScopeExe()
{
ExitScopeExe();
ExitExe(sessionPath);
StartExe(locationPath + sessionPath);
}
public WindowsDriver<WindowsElement> GetRoot()
{
return SessionHelper.root!;
}
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
public WindowsDriver<WindowsElement> GetDriver()
{
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
return this.Driver;
}
private void StartWindowsAppDriverApp()
{
var winAppDriverProcessInfo = new ProcessStartInfo
{
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
Verb = "runas",
};
this.ExitExe(winAppDriverProcessInfo.FileName);
SessionHelper.appDriver = Process.Start(winAppDriverProcessInfo);
}
}
}

View File

@@ -8,9 +8,6 @@
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
<ItemGroup>

View File

@@ -6,16 +6,10 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using Windows.Devices.Display.Core;
using Windows.Foundation.Metadata;
using static Microsoft.PowerToys.UITest.UITestBase.NativeMethods;
using static Microsoft.PowerToys.UITest.WindowHelper;
namespace Microsoft.PowerToys.UITest
{
@@ -23,37 +17,19 @@ namespace Microsoft.PowerToys.UITest
/// Base class that should be inherited by all Test Classes.
/// </summary>
[TestClass]
public class UITestBase : IDisposable
public class UITestBase
{
public required TestContext TestContext { get; set; }
public Session Session { get; set; }
public required Session Session { get; set; }
public bool IsInPipeline { get; }
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
private readonly SessionHelper sessionHelper;
private readonly PowerToysModule scope;
private readonly WindowSize size;
private SessionHelper? sessionHelper;
private System.Threading.Timer? screenshotTimer;
private string? screenshotDirectory;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified)
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
{
this.IsInPipeline = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform"));
Console.WriteLine($"Running tests on platform: {Environment.GetEnvironmentVariable("platform")}");
if (IsInPipeline)
{
NativeMethods.ChangeDisplayResolution(1920, 1080);
NativeMethods.GetMonitorInfo();
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
this.scope = scope;
this.size = size;
this.sessionHelper = new SessionHelper(scope).Init();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
}
/// <summary>
@@ -62,22 +38,6 @@ namespace Microsoft.PowerToys.UITest
[TestInitialize]
public void TestInit()
{
CloseOtherApplications();
if (IsInPipeline)
{
screenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
Directory.CreateDirectory(screenshotDirectory);
// Take screenshot every 1 second
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, screenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
// Escape Popups before starting
System.Windows.Forms.SendKeys.SendWait("{ESC}");
}
this.sessionHelper = new SessionHelper(scope).Init();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), scope, size);
if (this.scope == PowerToysModule.PowerToysSettings)
{
// close Debug warning dialog if any
@@ -90,32 +50,12 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Cleanups the test.
/// UnInitializes the test.
/// </summary>
[TestCleanup]
public void TestCleanup()
public void TestClean()
{
if (IsInPipeline)
{
screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite);
Dispose();
if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed
or UnitTestOutcome.Error
or UnitTestOutcome.Unknown)
{
Task.Delay(1000).Wait();
AddScreenShotsToTestResultsDirectory();
}
}
this.Session.Cleanup();
this.sessionHelper!.Cleanup();
}
public void Dispose()
{
screenshotTimer?.Dispose();
GC.SuppressFinalize(this);
this.sessionHelper.Cleanup();
}
/// <summary>
@@ -124,143 +64,47 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected T Find<T>(By by, int timeoutMS = 5000, bool global = false)
protected T Find<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.Find<T>(by, timeoutMS, global);
return this.Session.Find<T>(by, timeoutMS);
}
/// <summary>
/// Shortcut for this.Session.Find<Element>(name, timeoutMS)
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected T Find<T>(string name, int timeoutMS = 5000, bool global = false)
protected T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.Find<T>(By.Name(name), timeoutMS, global);
return this.Session.Find<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Shortcut for this.Session.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected Element Find(By by, int timeoutMS = 5000, bool global = false)
protected Element Find(By by, int timeoutMS = 3000)
{
return this.Session.Find(by, timeoutMS, global);
return this.Session.Find(by, timeoutMS);
}
/// <summary>
/// Shortcut for this.Session.Find<Element>(name, timeoutMS)
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected Element Find(string name, int timeoutMS = 5000, bool global = false)
protected Element Find(string name, int timeoutMS = 3000)
{
return this.Session.Find(name, timeoutMS, global);
}
/// <summary>
/// Has only one Element or its derived class by selector.
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne<T>(By by, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.FindAll<T>(by, timeoutMS, global).Count == 1;
}
/// <summary>
/// Shortcut for this.Session.HasOne<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne(By by, int timeoutMS = 5000, bool global = false)
{
return this.Session.HasOne<Element>(by, timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Session.HasOne<T>(name, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne<T>(string name, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.Session.HasOne<T>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Session.HasOne<Element>(name, timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if only has one element, otherwise false.</returns>
public bool HasOne(string name, int timeoutMS = 5000, bool global = false)
{
return this.Session.HasOne<Element>(name, timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Session.Has<T>(by, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has<T>(By by, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.Session.FindAll<T>(by, timeoutMS, global).Count >= 1;
}
/// <summary>
/// Shortcut for this.Session.Has<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has(By by, int timeoutMS = 5000, bool global = false)
{
return this.Session.Has<Element>(by, timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Session.Has<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has<T>(string name, int timeoutMS = 5000, bool global = false)
where T : Element, new()
{
return this.Session.Has<T>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Shortcut for this.Session.Has<Element>(name, timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <returns>True if has one or more element, otherwise false.</returns>
public bool Has(string name, int timeoutMS = 5000, bool global = false)
{
return this.Session.Has<Element>(name, timeoutMS, global);
return this.Session.Find(name, timeoutMS);
}
/// <summary>
@@ -269,12 +113,12 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000, bool global = false)
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.FindAll<T>(by, timeoutMS, global);
return this.Session.FindAll<T>(by, timeoutMS);
}
/// <summary>
@@ -283,12 +127,12 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000, bool global = false)
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.FindAll<T>(By.Name(name), timeoutMS, global);
return this.Session.FindAll<T>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -296,11 +140,11 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Session.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000, bool global = false)
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.Session.FindAll<Element>(by, timeoutMS, global);
return this.Session.FindAll<Element>(by, timeoutMS);
}
/// <summary>
@@ -308,137 +152,11 @@ namespace Microsoft.PowerToys.UITest
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000, bool global = false)
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.Session.FindAll<Element>(By.Name(name), timeoutMS, global);
}
/// <summary>
/// Scrolls the page
/// </summary>
/// <param name="scrollCount">The number of scroll attempts.</param>
/// <param name="direction">The direction to scroll.</param>
/// <param name="msPreAction">Pre-action delay in milliseconds.</param>
/// <param name="msPostAction">Post-action delay in milliseconds.</param>
public void Scroll(int scrollCount = 5, string direction = "Up", int msPreAction = 500, int msPostAction = 500)
{
MouseActionType mouseAction = direction == "Up" ? MouseActionType.ScrollUp : MouseActionType.ScrollDown;
for (int i = 0; i < scrollCount; i++)
{
Session.PerformMouseAction(mouseAction, msPreAction, msPostAction); // Ensure settings are visible
}
}
/// <summary>
/// Captures the last screenshot when the test fails.
/// </summary>
protected void CaptureLastScreenshot()
{
// Implement your screenshot capture logic here
// For example, save a screenshot to a file and return the file path
string screenshotPath = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "last_screenshot.png");
this.Session.Root.GetScreenshot().SaveAsFile(screenshotPath, ScreenshotImageFormat.Png);
// Save screenshot to screenshotPath & upload to test attachment
this.TestContext.AddResultFile(screenshotPath);
}
/// <summary>
/// Retrieves the color of the pixel at the specified screen coordinates.
/// </summary>
/// <param name="x">The X coordinate on the screen.</param>
/// <param name="y">The Y coordinate on the screen.</param>
/// <returns>The color of the pixel at the specified coordinates.</returns>
public Color GetPixelColor(int x, int y)
{
return WindowHelper.GetPixelColor(x, y);
}
/// <summary>
/// Retrieves the color of the pixel at the specified screen coordinates as a string.
/// </summary>
/// <param name="x">The X coordinate on the screen.</param>
/// <param name="y">The Y coordinate on the screen.</param>
/// <returns>The color of the pixel at the specified coordinates.</returns>
public string GetPixelColorString(int x, int y)
{
return WindowHelper.GetPixelColorString(x, y);
}
/// <summary>
/// Gets the size of the display.
/// </summary>
/// <returns>
/// A tuple containing the width and height of the display.
/// </returns
public Tuple<int, int> GetDisplaySize()
{
return WindowHelper.GetDisplaySize();
}
/// <summary>
/// Sends a combination of keys.
/// </summary>
/// <param name="keys">The keys to send.</param>
public void SendKeys(params Key[] keys)
{
this.Session.SendKeys(keys);
}
/// <summary>
/// Sends a sequence of keys.
/// </summary>
/// <param name="keys">An array of keys to send.</param>
public void SendKeySequence(params Key[] keys)
{
this.Session.SendKeySequence(keys);
}
/// <summary>
        /// Gets the current position of the mouse cursor as a tuple.
        /// </summary>
        /// <returns>A tuple containing the X and Y coordinates of the cursor.</returns>
public Tuple<int, int> GetMousePosition()
{
return this.Session.GetMousePosition();
}
/// <summary>
/// Gets the screen center coordinates.
/// </summary>
/// <returns>(x, y)</returns>
public (int CenterX, int CenterY) GetScreenCenter()
{
return WindowHelper.GetScreenCenter();
}
public bool IsWindowOpen(string windowName)
{
return WindowHelper.IsWindowOpen(windowName);
}
/// <summary>
    /// Moves the mouse cursor to the specified screen coordinates.
    /// </summary>
    /// <param name="x">The new x-coordinate of the cursor.</param>
    /// <param name="y">The new y-coordinate of the cursor.</param
public void MoveMouseTo(int x, int y)
{
        this.Session.MoveMouseTo(x, y);
}
protected void AddScreenShotsToTestResultsDirectory()
{
if (screenshotDirectory != null)
{
foreach (string file in Directory.GetFiles(screenshotDirectory))
{
this.TestContext.AddResultFile(file);
}
}
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -446,8 +164,8 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public void RestartScopeExe()
{
this.sessionHelper!.RestartScopeExe();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
this.sessionHelper.RestartScopeExe();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
return;
}
@@ -456,194 +174,8 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public void ExitScopeExe()
{
this.sessionHelper!.ExitScopeExe();
this.sessionHelper.ExitScopeExe();
return;
}
private void CloseOtherApplications()
{
// Close other applications
var processNamesToClose = new List<string>
{
"PowerToys",
"PowerToys.Settings",
"PowerToys.FancyZonesEditor",
};
foreach (var processName in processNamesToClose)
{
foreach (var process in Process.GetProcessesByName(processName))
{
process.Kill();
process.WaitForExit();
}
}
}
public class NativeMethods
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DISPLAY_DEVICE
{
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceID;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string DeviceKey;
}
[DllImport("user32.dll")]
private static extern int EnumDisplaySettings(IntPtr deviceName, int modeNum, ref DEVMODE devMode);
[DllImport("user32.dll")]
private static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
private static extern bool EnumDisplayDevices(IntPtr lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, int dwFlags);
[DllImport("user32.dll")]
private static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
private static extern int ChangeDisplaySettingsEx(IntPtr lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam);
private const int DM_PELSWIDTH = 0x80000;
private const int DM_PELSHEIGHT = 0x100000;
public const int ENUM_CURRENT_SETTINGS = -1;
public const int CDS_TEST = 0x00000002;
public const int CDS_UPDATEREGISTRY = 0x01;
public const int DISP_CHANGE_SUCCESSFUL = 0;
public const int DISP_CHANGE_RESTART = 1;
public const int DISP_CHANGE_FAILED = -1;
[StructLayout(LayoutKind.Sequential)]
public struct DEVMODE
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string DmDeviceName;
public short DmSpecVersion;
public short DmDriverVersion;
public short DmSize;
public short DmDriverExtra;
public int DmFields;
public int DmPositionX;
public int DmPositionY;
public int DmDisplayOrientation;
public int DmDisplayFixedOutput;
public short DmColor;
public short DmDuplex;
public short DmYResolution;
public short DmTTOption;
public short DmCollate;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string DmFormName;
public short DmLogPixels;
public int DmBitsPerPel;
public int DmPelsWidth;
public int DmPelsHeight;
public int DmDisplayFlags;
public int DmDisplayFrequency;
public int DmICMMethod;
public int DmICMIntent;
public int DmMediaType;
public int DmDitherType;
public int DmReserved1;
public int DmReserved2;
public int DmPanningWidth;
public int DmPanningHeight;
}
public static void GetMonitorInfo()
{
int deviceIndex = 0;
DISPLAY_DEVICE d = default(DISPLAY_DEVICE);
d.cb = Marshal.SizeOf(d);
Console.WriteLine("monitor list :");
while (EnumDisplayDevices(IntPtr.Zero, deviceIndex, ref d, 0))
{
Console.WriteLine($"monitor {deviceIndex + 1}:");
Console.WriteLine($" name: {d.DeviceName}");
Console.WriteLine($"  string: {d.DeviceString}");
Console.WriteLine($"  ID: {d.DeviceID}");
Console.WriteLine($"  key: {d.DeviceKey}");
Console.WriteLine();
DEVMODE dm = default(DEVMODE);
dm.DmSize = (short)Marshal.SizeOf<DEVMODE>();
int modeNum = 0;
while (EnumDisplaySettings(d.DeviceName, modeNum, ref dm) > 0)
{
MonitorInfoData.Monitors.Add(new MonitorInfoData.MonitorInfoDataWrapper()
{
DeviceName = d.DeviceName,
DeviceString = d.DeviceString,
DeviceID = d.DeviceID,
DeviceKey = d.DeviceKey,
PelsWidth = dm.DmPelsWidth,
PelsHeight = dm.DmPelsHeight,
DisplayFrequency = dm.DmDisplayFrequency,
});
Console.WriteLine($"  mode {modeNum}: {dm.DmPelsWidth}x{dm.DmPelsHeight} @ {dm.DmDisplayFrequency}Hz");
modeNum++;
}
deviceIndex++;
d.cb = Marshal.SizeOf(d); // Reset the size for the next device
}
}
public static void ChangeDisplayResolution(int PelsWidth, int PelsHeight)
{
Screen screen = Screen.PrimaryScreen!;
if (screen.Bounds.Width == PelsWidth && screen.Bounds.Height == PelsHeight)
{
return;
}
DEVMODE devMode = default(DEVMODE);
devMode.DmDeviceName = new string(new char[32]);
devMode.DmFormName = new string(new char[32]);
devMode.DmSize = (short)Marshal.SizeOf<DEVMODE>();
int modeNum = 0;
while (EnumDisplaySettings(IntPtr.Zero, modeNum, ref devMode) > 0)
{
Console.WriteLine($"Mode {modeNum}: {devMode.DmPelsWidth}x{devMode.DmPelsHeight} @ {devMode.DmDisplayFrequency}Hz");
modeNum++;
}
devMode.DmPelsWidth = PelsWidth;
devMode.DmPelsHeight = PelsHeight;
int result = NativeMethods.ChangeDisplaySettings(ref devMode, NativeMethods.CDS_TEST);
if (result == DISP_CHANGE_SUCCESSFUL)
{
result = ChangeDisplaySettings(ref devMode, CDS_UPDATEREGISTRY);
if (result == DISP_CHANGE_SUCCESSFUL)
{
Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight}");
}
else
{
Console.WriteLine($"Failed to change display resolution. Error code: {result}");
}
}
else if (result == DISP_CHANGE_RESTART)
{
Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight} requires a restart");
}
else
{
Console.WriteLine($"Failed to change display resolution. Error code: {result}");
}
}
}
}
}

View File

@@ -1,160 +0,0 @@
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.UITest
{
public static class VisualAssert
{
/// <summary>
/// Asserts current visual state of the element is equal with base line image.
/// To use this VisualAssert, you need to set Window Theme to Light-Mode to avoid Theme color difference in baseline image.
/// Such limitation could be removed either Auto-generate baseline image for both Light & Dark mode
/// </summary>
/// <param name="testContext">TestContext object</param>
/// <param name="element">Element object</param>
/// <param name="scenarioSubname">additional scenario name if two or more scenarios in one test</param>
[RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "")
{
var pipelinePlatform = Environment.GetEnvironmentVariable("platform");
// Perform visual validation only in the pipeline
if (string.IsNullOrEmpty(pipelinePlatform))
{
Console.WriteLine("Skip visual validation in the local run.");
return;
}
if (element == null)
{
Assert.Fail("Element object is null or invalid");
}
var stackTrace = new StackTrace();
var callerFrame = stackTrace.GetFrame(1);
var callerMethod = callerFrame?.GetMethod();
var callerName = callerMethod?.Name;
var callerClassName = callerMethod?.DeclaringType?.Name;
if (string.IsNullOrEmpty(callerName) || string.IsNullOrEmpty(callerClassName))
{
Assert.Fail("Unable to determine the caller method and class name.");
}
if (string.IsNullOrWhiteSpace(scenarioSubname))
{
scenarioSubname = string.Join("_", callerClassName, callerName, pipelinePlatform);
}
else
{
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), pipelinePlatform);
}
var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault();
var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png");
element.SaveToPngFile(tempTestImagePath);
if (string.IsNullOrEmpty(baselineImageResourceName)
|| !Path.GetFileNameWithoutExtension(baselineImageResourceName).EndsWith(scenarioSubname))
{
testContext?.AddResultFile(tempTestImagePath);
Assert.Fail($"Baseline image for scenario {scenarioSubname} can't be found, test image saved in file://{tempTestImagePath.Replace('\\', '/')}");
}
var tempBaselineImagePath = GetTempFilePath(scenarioSubname, "baseline", Path.GetExtension(baselineImageResourceName));
bool isSame = false;
using (var stream = callerMethod!.DeclaringType!.Assembly.GetManifestResourceStream(baselineImageResourceName))
{
if (stream == null)
{
Assert.Fail($"Resource stream '{baselineImageResourceName}' is null.");
}
using (var baselineImage = new Bitmap(stream))
{
using (var testImage = new Bitmap(tempTestImagePath))
{
isSame = VisualAssert.AreEqual(baselineImage, testImage);
if (!isSame)
{
// Copy baseline image to temp folder as well
baselineImage.Save(tempBaselineImagePath);
}
}
}
}
if (!isSame)
{
if (testContext != null)
{
testContext.AddResultFile(tempBaselineImagePath);
testContext.AddResultFile(tempTestImagePath);
}
Assert.Fail($"Fail to validate visual result for scenario {scenarioSubname}, baseline image can be found file://{tempBaselineImagePath.Replace('\\', '/')}, and test image can be found file://{tempTestImagePath.Replace('\\', '/')}");
}
}
/// <summary>
/// Get temp file path
/// </summary>
/// <param name="scenario">scenario name</param>
/// <param name="imageType">baseline or test image</param>
/// <param name="extension">image file extension</param>
/// <returns>full temp file path</returns>
private static string GetTempFilePath(string scenario, string imageType, string extension)
{
var tempFileFullName = $"{scenario}_{imageType}{extension}";
// Remove invalid filename character if any
Path.GetInvalidFileNameChars().ToList().ForEach(c => tempFileFullName = tempFileFullName.Replace(c, '-'));
return Path.Combine(Path.GetTempPath(), tempFileFullName);
}
/// <summary>
/// Test if two images are equal bit-by-bit
/// </summary>
/// <param name="baselineImage">baseline image</param>
/// <param name="testImage">test image</param>
/// <returns>true if are equal,otherwise false</returns>
private static bool AreEqual(Bitmap baselineImage, Bitmap testImage)
{
if (baselineImage.Width != testImage.Width || baselineImage.Height != testImage.Height)
{
return false;
}
// WinAppDriver sometimes adds a border to the screenshot (around 2 pix width), and it is not always consistent.
// So we exclude the border when comparing the images, and usually it is the edge of the windows, won't affect the comparison.
int excludeBorderWidth = 5, excludeBorderHeight = 5;
for (int x = excludeBorderWidth; x < baselineImage.Width - excludeBorderWidth; x++)
{
for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++)
{
if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y)))
{
return false;
}
}
}
return true;
}
}
}

View File

@@ -1,33 +0,0 @@
// 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.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.PowerToys.UITest
{
internal static class VisualHelper
{
/// <summary>
/// Compare two pixels with a fuzz factor
/// </summary>
/// <param name="c1">base color</param>
/// <param name="c2">test color</param>
/// <param name="fuzz">fuzz factor, default is 10</param>
/// <returns>true if same, otherwise is false</returns>
public static bool PixIsSame(Color c1, Color c2, int fuzz = 10)
{
return Math.Abs(c1.A - c2.A) <= fuzz && Math.Abs(c1.R - c2.R) <= fuzz && Math.Abs(c1.G - c2.G) <= fuzz && Math.Abs(c1.B - c2.B) <= fuzz;
}
}
}

View File

@@ -1,331 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerToys.UITest
{
internal static class WindowHelper
{
internal const string AdministratorPrefix = "Administrator: ";
/// <summary>
/// Sets the main window size.
/// </summary>
/// <param name="size">WindowSize enum</param>
public static void SetWindowSize(IntPtr windowHandler, WindowSize size)
{
if (size == WindowSize.UnSpecified)
{
return;
}
int width = 0, height = 0;
switch (size)
{
case WindowSize.Small:
width = 640;
height = 480;
break;
case WindowSize.Small_Vertical:
width = 480;
height = 640;
break;
case WindowSize.Medium:
width = 1024;
height = 768;
break;
case WindowSize.Medium_Vertical:
width = 768;
height = 1024;
break;
case WindowSize.Large:
width = 1920;
height = 1080;
break;
case WindowSize.Large_Vertical:
width = 1080;
height = 1920;
break;
}
if (width > 0 && height > 0)
{
WindowHelper.SetMainWindowSize(windowHandler, width, height);
}
}
/// <summary>
/// Gets the main window center coordinates.
/// </summary>
/// <returns>(x, y)</returns>
public static (int CenterX, int CenterY) GetWindowCenter(IntPtr windowHandler)
{
if (windowHandler == IntPtr.Zero)
{
return (0, 0);
}
else
{
var rect = ApiHelper.GetWindowCenter(windowHandler);
return (rect.CenterX, rect.CenterY);
}
}
/// <summary>
/// Gets the main window center coordinates.
/// </summary>
/// <returns>(x, y)</returns>
public static (int Left, int Top, int Right, int Bottom) GetWindowRect(IntPtr windowHandler)
{
if (windowHandler == IntPtr.Zero)
{
return (0, 0, 0, 0);
}
else
{
var rect = ApiHelper.GetWindowRect(windowHandler);
return (rect.Left, rect.Top, rect.Right, rect.Bottom);
}
}
/// <summary>
/// Gets the screen center coordinates.
/// </summary>
/// <returns>(x, y)</returns>
public static (int CenterX, int CenterY) GetScreenCenter()
{
return ApiHelper.GetScreenCenter();
}
/// <summary>
/// Sets the main window size based on Width and Height.
/// </summary>
/// <param name="width">the width in pixel</param>
/// <param name="height">the height in pixel</param>
public static void SetMainWindowSize(IntPtr windowHandler, int width, int height)
{
if (windowHandler == IntPtr.Zero
|| width <= 0
|| height <= 0)
{
return;
}
ApiHelper.SetWindowPos(windowHandler, IntPtr.Zero, 0, 0, width, height, ApiHelper.SetWindowPosNoZorder | ApiHelper.SetWindowPosShowWindow);
// Wait for 1000ms after resize
Task.Delay(1000).Wait();
}
/// <summary>
/// Retrieves the color of the pixel at the specified screen coordinates.
/// </summary>
/// <param name="x">The X coordinate on the screen.</param>
/// <param name="y">The Y coordinate on the screen.</param>
/// <returns>The color of the pixel at the specified coordinates.</returns>
public static Color GetPixelColor(int x, int y)
{
IntPtr hdc = ApiHelper.GetDC(IntPtr.Zero);
uint pixel = ApiHelper.GetPixel(hdc, x, y);
_ = ApiHelper.ReleaseDC(IntPtr.Zero, hdc);
int r = (int)(pixel & 0x000000FF);
int g = (int)((pixel & 0x0000FF00) >> 8);
int b = (int)((pixel & 0x00FF0000) >> 16);
return Color.FromArgb(r, g, b);
}
/// <summary>
/// Retrieves the color of the pixel at the specified screen coordinates as a string.
/// </summary>
/// <param name="x">The X coordinate on the screen.</param>
/// <param name="y">The Y coordinate on the screen.</param>
/// <returns>The color of the pixel at the specified coordinates.</returns>
public static string GetPixelColorString(int x, int y)
{
Color color = WindowHelper.GetPixelColor(x, y);
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
}
/// <summary>
/// Gets the size of the display.
/// </summary>
/// <returns>
/// A tuple containing the width and height of the display.
/// </returns
public static Tuple<int, int> GetDisplaySize()
{
IntPtr hdc = ApiHelper.GetDC(IntPtr.Zero);
int screenWidth = ApiHelper.GetDeviceCaps(hdc, ApiHelper.DESKTOPHORZRES);
int screenHeight = ApiHelper.GetDeviceCaps(hdc, ApiHelper.DESKTOPVERTRES);
_ = ApiHelper.ReleaseDC(IntPtr.Zero, hdc);
return Tuple.Create(screenWidth, screenHeight);
}
public static bool IsWindowOpen(string windowName)
{
var matchingWindows = ApiHelper.FindDesktopWindowHandler([windowName, AdministratorPrefix + windowName]);
return matchingWindows.Count > 0;
}
internal static class ApiHelper
{
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
public const uint SetWindowPosNoMove = 0x0002;
public const uint SetWindowPosNoZorder = 0x0004;
public const uint SetWindowPosShowWindow = 0x0040;
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
// Delegate for the EnumWindows callback function
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
// P/Invoke declaration for EnumWindows
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
// P/Invoke declaration for GetWindowTextLength
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int GetWindowTextLength(IntPtr hWnd);
// P/Invoke declaration for GetWindowText
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("gdi32.dll")]
public static extern uint GetPixel(IntPtr hdc, int x, int y);
[DllImport("gdi32.dll")]
public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
public const int DESKTOPHORZRES = 118;
public const int DESKTOPVERTRES = 117;
[DllImport("user32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
// Define the Win32 RECT structure
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left; // X coordinate of the left edge of the window
public int Top; // Y coordinate of the top edge of the window
public int Right; // X coordinate of the right edge of the window
public int Bottom; // Y coordinate of the bottom edge of the window
}
// Import GetWindowRect API to retrieve window's screen coordinates
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
public static List<(IntPtr HWnd, string Title)> FindDesktopWindowHandler(string[] matchingWindowsTitles)
{
var windows = new List<(IntPtr HWnd, string Title)>();
_ = EnumWindows(
(hWnd, lParam) =>
{
int length = GetWindowTextLength(hWnd);
if (length > 0)
{
var builder = new StringBuilder(length + 1);
_ = GetWindowText(hWnd, builder, builder.Capacity);
var title = builder.ToString();
if (matchingWindowsTitles.Contains(title))
{
windows.Add((hWnd, title));
}
}
return true; // Continue enumeration
},
IntPtr.Zero);
return windows;
}
/// <summary>
/// Get the center point coordinates of a specified window (in screen coordinates)
/// </summary>
/// <param name="hWnd">The window handle</param>
/// <returns>The center point (x, y)</returns>
public static (int CenterX, int CenterY) GetWindowCenter(IntPtr hWnd)
{
if (hWnd == IntPtr.Zero)
{
throw new ArgumentException("Invalid window handle");
}
if (GetWindowRect(hWnd, out RECT rect))
{
int width = rect.Right - rect.Left;
int height = rect.Bottom - rect.Top;
int centerX = rect.Left + (width / 2);
int centerY = rect.Top + (height / 2);
return (centerX, centerY);
}
else
{
throw new InvalidOperationException("Failed to retrieve window coordinates");
}
}
public static (int Left, int Top, int Right, int Bottom) GetWindowRect(IntPtr hWnd)
{
if (hWnd == IntPtr.Zero)
{
throw new ArgumentException("Invalid window handle");
}
if (GetWindowRect(hWnd, out RECT rect))
{
return (rect.Left, rect.Top, rect.Right, rect.Bottom);
}
else
{
throw new InvalidOperationException("Failed to retrieve window coordinates");
}
}
[DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nIndex);
public enum SystemMetric
{
ScreenWidth = 0, // Width of the primary screen in pixels (SM_CXSCREEN)
ScreenHeight = 1, // Height of the primary screen in pixels (SM_CYSCREEN)
VirtualScreenWidth = 78, // Width of the virtual screen that includes all monitors (SM_CXVIRTUALSCREEN)
VirtualScreenHeight = 79, // Height of the virtual screen that includes all monitors (SM_CYVIRTUALSCREEN)
MonitorCount = 80, // Number of display monitors (SM_CMONITORS, available on Windows XP+)
}
public static (int CenterX, int CenterY) GetScreenCenter()
{
int width = GetSystemMetrics((int)SystemMetric.ScreenWidth);
int height = GetSystemMetrics((int)SystemMetric.ScreenHeight);
return (width / 2, height / 2);
}
}
}
}

View File

@@ -154,9 +154,6 @@
<ProjectReference Include="..\..\..\common\Telemetry\EtwTrace\EtwTrace.vcxproj">
<Project>{8f021b46-362b-485c-bfba-ccf83e820cbd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\Themes\Themes.vcxproj">
<Project>{98537082-0fdb-40de-abd8-0dc5a4269bab}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />

View File

@@ -17,9 +17,6 @@
#include <common/Telemetry/EtwTrace/EtwTrace.h>
#include <common/Themes/theme_helpers.h>
#include <common/Themes/theme_listener.h>
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
namespace winrt
@@ -38,23 +35,6 @@ namespace util
const std::wstring instanceMutexName = L"Local\\PowerToys_CropAndLock_InstanceMutex";
bool m_running = true;
// Theming
ThemeListener theme_listener{};
// Keep a list of our cropped windows
std::vector<std::shared_ptr<CropAndLockWindow>> croppedWindows;
void handleTheme()
{
auto theme = theme_listener.AppTheme;
auto isDark = theme == Theme::Dark;
Logger::info(L"Theme is now {}", isDark ? L"Dark" : L"Light");
for (auto&& croppedWindow : croppedWindows)
{
ThemeHelpers::SetImmersiveDarkMode(croppedWindow->Handle(), isDark);
}
}
int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _In_ int)
{
// Initialize COM
@@ -62,8 +42,6 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
Trace::CropAndLock::RegisterProvider();
theme_listener.AddChangedHandler(handleTheme);
Shared::Trace::ETWTrace trace;
trace.UpdateState(true);
@@ -129,6 +107,8 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
// Create our overlay window
std::unique_ptr<OverlayWindow> overlayWindow;
// Keep a list of our cropped windows
std::vector<std::shared_ptr<CropAndLockWindow>> croppedWindows;
// Handles and thread for the events sent from runner
HANDLE m_reparent_event_handle;
@@ -187,7 +167,6 @@ int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _I
croppedWindow->CropAndLock(targetWindow, cropRect);
croppedWindow->OnClosed(removeWindowCallback);
croppedWindows.push_back(croppedWindow);
handleTheme();
};
overlayWindow.reset();

View File

@@ -6,7 +6,6 @@
<PropertyGroup>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<DefineConstants>TESTONLY</DefineConstants>
</PropertyGroup>
<PropertyGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -13,7 +13,7 @@ namespace Hosts.UITests
public class HostModuleTests : UITestBase
{
public HostModuleTests()
: base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
: base(PowerToysModule.Hosts)
{
}
@@ -31,17 +31,14 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.EmptyViewShouldWork")]
[TestCategory("Hosts File Editor #4")]
[TestMethod]
public void TestEmptyView()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
// 'Add an entry' button (only show-up when list is empty) should be visible
Assert.IsTrue(this.HasOne<HyperlinkButton>("Add an entry"), "'Add an entry' button should be visible in the empty view");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "EmptyView");
Assert.IsTrue(this.FindAll<HyperlinkButton>("Add an entry").Count == 1, "'Add an entry' button should be visible in the empty view");
// Click 'Add an entry' from empty-view for adding Host override rule
this.Find<HyperlinkButton>("Add an entry").Click();
@@ -49,10 +46,8 @@ namespace Hosts.UITests
this.AddEntry("192.168.0.1", "localhost", false, false);
// Should have one row now and not more empty view
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
Assert.IsFalse(this.Has<HyperlinkButton>("Add an entry"), "'Add an entry' button should be invisible if not empty view");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "NonEmptyView");
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 1, "Should have one row now");
Assert.IsTrue(this.FindAll<HyperlinkButton>("Add an entry").Count == 0, "'Add an entry' button should be invisible if not empty view");
}
/// <summary>
@@ -63,20 +58,17 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.AddEntryButtonShouldWork")]
[TestCategory("Hosts File Editor #4")]
[TestMethod]
public void TestAddingEntry()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
Assert.IsFalse(this.Has<Button>("Delete"), "Should have no row after removing all");
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0, "Should have no row after removing all");
this.AddEntry("192.168.0.1", "localhost", true);
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"));
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 1, "Should have one row now");
}
/// <summary>
@@ -90,8 +82,7 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.CanNotAddMoreThenNighHosts")]
[TestCategory("Hosts File Editor #5")]
[TestMethod]
public void TestTooManyHosts()
{
this.CloseWarningDialog();
@@ -125,48 +116,18 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.ErrorMessageShowupIfNotRunAsAdmin")]
[TestCategory("Hosts File Editor #8")]
[TestMethod]
public void TestErrorMessageWithNonAdminPermission()
{
if (this.Session.IsElevated == false)
{
this.CloseWarningDialog();
this.RemoveAllEntries();
this.CloseWarningDialog();
this.RemoveAllEntries();
// Add new URL override and a warning tip should be shown
this.AddEntry("192.168.0.1", "localhost", true);
// Add new URL override and a warning tip should be shown
this.AddEntry("192.168.0.1", "localhost", true);
Assert.IsTrue(
this.FindAll<TextBlock>("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1,
"Should display host-file saving error if not run as administrator");
}
}
/// <summary>
/// Test No Error-message in the Hosts-File-Editor
/// <list type="bullet">
/// <item>
/// <description>Validating error message should be shown if not run as admin.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.NoErrorMessageShowupIfRunAsAdmin")]
[TestCategory("Hosts File Editor #8")]
public void TestNoErrorMessageWithNonAdminPermission()
{
if (this.Session.IsElevated == true)
{
this.CloseWarningDialog();
this.RemoveAllEntries();
// Add new URL override and a warning tip should be shown
this.AddEntry("192.168.0.1", "localhost", true);
Assert.IsFalse(
this.FindAll<TextBlock>("The hosts file cannot be saved because the program isn't running as administrator.").Count > 0,
"Should display host-file saving error if not run as administrator");
}
Assert.IsTrue(
this.FindAll<TextBlock>("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1,
"Should display host-file saving error if not run as administrator");
}
/// <summary>
@@ -183,8 +144,7 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Basic.FiltersControlShouldWork")]
[TestCategory("Hosts File Editor #6")]
[TestMethod]
public void TestFilterControl()
{
this.CloseWarningDialog();
@@ -252,7 +212,7 @@ namespace Hosts.UITests
// Close-filter-panel
this.Find<Button>("Filters").Click();
Assert.IsFalse(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be closed after clicking Filter Button");
Assert.IsFalse(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be closed afer click Filter Button");
}
private void AddEntry(string ip, string host, bool active = true, bool clickAddEntryButton = true)
@@ -283,25 +243,25 @@ namespace Hosts.UITests
private void CloseWarningDialog()
{
// Find 'Accept' button which come in 'Warning' dialog
if (this.FindAll("Warning", 1000).Count > 0 &&
this.FindAll<Button>("Accept", 1000).Count > 0)
if (this.FindAll("Warning").Count > 0 &&
this.FindAll<Button>("Accept").Count > 0)
{
// Hide Warning dialog if any
this.Find<Button>("Accept", 1000).Click();
this.Find<Button>("Accept").Click();
}
}
private void RemoveAllEntries()
{
// Delete all existing host-override rules
foreach (var deleteBtn in this.FindAll<Button>("Delete", 1000))
foreach (var deleteBtn in this.FindAll<Button>("Delete"))
{
deleteBtn.Click();
this.Find<Button>("Yes", 1000).Click();
this.Find<Button>("Yes").Click();
}
// Should have no row left, and no more delete button
Assert.IsTrue(this.FindAll<Button>("Delete", 1000).Count == 0);
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0);
}
}
}

View File

@@ -17,17 +17,6 @@
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Baseline\HostModuleTests_TestAddingEntry_arm64.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_EmptyView_arm64.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_NonEmptyView_arm64.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestAddingEntry_x64Win10.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_EmptyView_x64Win10.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_NonEmptyView_x64Win10.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestAddingEntry_x64Win11.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_EmptyView_x64Win11.png" />
<EmbeddedResource Include="Baseline\HostModuleTests_TestEmptyView_NonEmptyView_x64Win11.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />

View File

@@ -12,11 +12,6 @@ namespace Hosts.UITests
[TestClass]
public class HostsSettingTests : UITestBase
{
public HostsSettingTests()
: base(PowerToysModule.PowerToysSettings, WindowSize.Medium)
{
}
/// <summary>
/// Test Warning Dialog at startup
/// <list type="bullet">
@@ -34,9 +29,7 @@ namespace Hosts.UITests
/// </item>
/// </list>
/// </summary>
[TestMethod("Hosts.Settings.ShowWarningDialogIfRunAsAdmin")]
[TestCategory("Hosts File Editor #1")]
[TestCategory("Hosts File Editor #9")]
[TestMethod]
public void TestWarningDialog()
{
this.LaunchFromSetting(showWarning: true);
@@ -58,7 +51,10 @@ namespace Hosts.UITests
this.Find<Button>("Launch Hosts File Editor").Click();
this.Session.Attach(PowerToysModule.Hosts, WindowSize.Small_Vertical);
// wait for 500 ms to make sure Hosts File Editor is launched
Task.Delay(500).Wait();
this.Session.Attach(PowerToysModule.Hosts);
// Should show warning dialog
Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
@@ -72,7 +68,7 @@ namespace Hosts.UITests
Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed after click Accept button in Warning Dialog");
// Close Hosts File Editor window
this.Session.CloseMainWindow();
this.Session.Find<Window>("Hosts File Editor").Close();
// Restore back to PowerToysSettings Session
this.Session.Attach(PowerToysModule.PowerToysSettings);
@@ -86,7 +82,7 @@ namespace Hosts.UITests
Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed");
// Close Hosts File Editor window
this.Session.CloseMainWindow();
this.Session.Find<Window>("Hosts File Editor").Close();
// Restore back to PowerToysSettings Session
this.Session.Attach(PowerToysModule.PowerToysSettings);
@@ -94,9 +90,14 @@ namespace Hosts.UITests
private bool IsHostsFileEditorClosed()
{
if (this.Session.FindAll<Window>("Hosts File Editor").Count == 0 && this.Session.FindAll<Window>("Administrator: Hosts File Editor").Count == 0)
try
{
return true;
this.Session.FindAll<Window>("Hosts File Editor");
}
catch (Exception ex)
{
// Validate if editor window closed by checking exception.Message
return ex.Message.Contains("Currently selected window has been closed");
}
return false;
@@ -120,7 +121,7 @@ namespace Hosts.UITests
// launch Hosts File Editor
this.Find<Button>("Launch Hosts File Editor").Click();
Task.Delay(2000).Wait();
Task.Delay(500).Wait();
this.Session.Attach(PowerToysModule.Hosts);
}

View File

@@ -611,7 +611,6 @@
<ContentDialog
x:Name="EntryDialog"
x:Uid="EntryDialog"
x:DataType="models:Entry"
IsPrimaryButtonEnabled="{Binding Valid, Mode=OneWay}"
Loaded="ContentDialog_Loaded_ApplyMargin"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">

View File

@@ -139,23 +139,10 @@ namespace HostsUILib.Views
dialog.XamlRoot = XamlRoot;
dialog.Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style;
dialog.Title = resourceLoader.GetString("WarningDialog_Title");
dialog.Content = new StackPanel
dialog.Content = new TextBlock
{
Children =
{
new TextBlock
{
Text = resourceLoader.GetString("WarningDialog_Text"),
TextWrapping = TextWrapping.Wrap,
},
new HyperlinkButton
{
Content = resourceLoader.GetString("WarningDialog_LearnMore"),
NavigateUri = new Uri("https://aka.ms/PowerToysOverview_HostsFileEditor"),
Padding = new Thickness(0),
Margin = new Thickness(0, 5, 0, 5),
},
},
Text = resourceLoader.GetString("WarningDialog_Text"),
TextWrapping = TextWrapping.Wrap,
};
dialog.PrimaryButtonText = resourceLoader.GetString("WarningDialog_AcceptBtn");
dialog.PrimaryButtonStyle = Application.Current.Resources["AccentButtonStyle"] as Style;

View File

@@ -11,9 +11,6 @@ using HostsUILib.Helpers;
namespace HostsUILib.Models
{
#if !TESTONLY
[Microsoft.UI.Xaml.Data.Bindable]
#endif
public partial class Entry : ObservableObject
{
private static readonly char[] _spaceCharacters = new char[] { ' ', '\t' };

View File

@@ -331,9 +331,6 @@
<data name="WarningDialog_Title" xml:space="preserve">
<value>Warning</value>
</data>
<data name="WarningDialog_LearnMore" xml:space="preserve">
<value>Learn more</value>
</data>
<data name="WindowAdminTitle" xml:space="preserve">
<value>Administrator: Hosts File Editor</value>
<comment>Title of the window when running as administrator. "Hosts File Editor" is the name of the utility. "Hosts" refers to the system hosts file, do not loc</comment>

View File

@@ -1,640 +0,0 @@
// 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.Globalization;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Devices.Printers;
namespace MouseUtils.UITests
{
[TestClass]
public class FindMyMouseTests : UITestBase
{
/// <summary>
/// Test Warning Dialog at startup
/// <list type="bullet">
/// <item>
/// <description>Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is On.</description>
/// </item>
/// <item>
/// <description>Validating Warning-Dialog will NOT be shown if 'Show a warning at startup' toggle is Off.</description>
/// </item>
/// <item>
/// <description>Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed.</description>
/// </item>
/// <item>
/// <description>Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod("MouseUtils.FindMyMouse.EnableFindMyMouse")]
[TestCategory("Mouse Utils #1")]
[TestCategory("Mouse Utils #2")]
[TestCategory("Mouse Utils #3")]
[TestCategory("Mouse Utils #4")]
public void TestEnableFindMyMouse()
{
LaunchFromSetting();
var settings = new FindMyMouseSettings();
settings.OverlayOpacity = "100";
settings.Radius = "50";
settings.InitialZoom = "1";
settings.AnimationDuration = "0";
settings.BackgroundColor = "000000";
settings.SpotlightColor = "FFFFFF";
var foundCustom = this.Find<Custom>("Find My Mouse");
Assert.IsNotNull(foundCustom);
if (CheckAnimationEnable(ref foundCustom))
{
foundCustom = this.Find<Custom>("Find My Mouse");
}
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
if (excludedApps != null)
{
excludedApps.Click();
excludedApps.Click();
}
else
{
Assert.Fail("Activation method group not found.");
}
}
else
{
Assert.Fail("Find My Mouse group not found.");
}
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
VerifySpotlightSettings(ref settings);
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press any other key and verify the overlay disappears.
Session.SendKeys(Key.A);
VerifySpotlightDisappears(ref settings);
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
VerifySpotlightSettings(ref settings);
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press a mouse button and verify the overlay disappears.
Task.Delay(1000).Wait();
Session.PerformMouseAction(MouseActionType.LeftClick, 500, 1000);
VerifySpotlightDisappears(ref settings);
}
[TestMethod("MouseUtils.FindMyMouse.FindMyMouseDifferentSettings")]
[TestCategory("Mouse Utils #10")]
[TestCategory("Mouse Utils #11")]
[TestCategory("Mouse Utils #12")]
public void TestFindMyMouseDifferentSettings()
{
LaunchFromSetting();
var settings = new FindMyMouseSettings();
settings.OverlayOpacity = "100";
settings.Radius = "80";
settings.InitialZoom = "1";
settings.AnimationDuration = "0";
settings.BackgroundColor = "FF0000";
settings.SpotlightColor = "0000FF";
var foundCustom = this.Find<Custom>("Find My Mouse");
Assert.IsNotNull(foundCustom);
if (CheckAnimationEnable(ref foundCustom))
{
foundCustom = this.Find<Custom>("Find My Mouse");
}
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
if (excludedApps != null)
{
excludedApps.Click();
excludedApps.Click();
}
else
{
Assert.Fail("Excluded apps group not found.");
}
}
else
{
Assert.Fail("Find My Mouse group not found.");
}
// [Test Case]Test the different settings and verify they apply, Background color
// [Test Case]Test the different settings and verify they apply, Spotlight color
// [Test Case]Test the different settings and verify they apply, Spotlight radius
VerifySpotlightSettings(ref settings);
Session.SendKeys(Key.A);
VerifySpotlightDisappears(ref settings);
}
[TestMethod("MouseUtils.FindMyMouse.DisableFindMyMouse")]
[TestCategory("Mouse Utils #5")]
[TestCategory("Mouse Utils #6")]
public void TestDisableFindMyMouse()
{
LaunchFromSetting();
var settings = new FindMyMouseSettings();
settings.OverlayOpacity = "100";
settings.Radius = "50";
settings.InitialZoom = "1";
settings.AnimationDuration = "0";
settings.BackgroundColor = "000000";
settings.SpotlightColor = "FFFFFF";
var foundCustom = this.Find<Custom>("Find My Mouse");
Assert.IsNotNull(foundCustom);
if (CheckAnimationEnable(ref foundCustom))
{
foundCustom = this.Find<Custom>("Find My Mouse");
}
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
Assert.IsNotNull(foundCustom);
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
if (excludedApps != null)
{
excludedApps.Click();
excludedApps.Click();
}
else
{
Assert.Fail("Activation method group not found.");
}
}
else
{
Assert.Fail("Find My Mouse group not found.");
}
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
// VerifySpotlightSettings(ref settings);
ActivateSpotlight(ref settings);
VerifySpotlightAppears(ref settings);
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
Task.Delay(1000).Wait();
ActivateSpotlight(ref settings);
VerifySpotlightDisappears(ref settings);
// [Test Case] Press Left Ctrl twice and verify the overlay appears
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
Task.Delay(2000).Wait();
ActivateSpotlight(ref settings);
VerifySpotlightAppears(ref settings);
Session.PerformMouseAction(MouseActionType.LeftClick);
}
[TestMethod("MouseUtils.FindMyMouse.DisableFindMyMouse3")]
[TestCategory("Mouse Utils #6")]
public void TestDisableFindMyMouse3()
{
LaunchFromSetting();
var settings = new FindMyMouseSettings();
settings.OverlayOpacity = "100";
settings.Radius = "50";
settings.InitialZoom = "1";
settings.AnimationDuration = "0";
settings.BackgroundColor = "000000";
settings.SpotlightColor = "FFFFFF";
var foundCustom = this.Find<Custom>("Find My Mouse");
Assert.IsNotNull(foundCustom);
if (CheckAnimationEnable(ref foundCustom))
{
foundCustom = this.Find<Custom>("Find My Mouse");
}
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
Assert.IsNotNull(foundCustom);
SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
if (excludedApps != null)
{
excludedApps.Click();
excludedApps.Click();
}
else
{
Assert.Fail("Activation method group not found.");
}
}
else
{
Assert.Fail("Find My Mouse group not found.");
}
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
// VerifySpotlightSettings(ref settings);
ActivateSpotlight(ref settings);
VerifySpotlightAppears(ref settings);
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
Task.Delay(1000).Wait();
ActivateSpotlight(ref settings);
VerifySpotlightDisappears(ref settings);
// [Test Case] Press Left Ctrl twice and verify the overlay appears
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
Task.Delay(2000).Wait();
ActivateSpotlight(ref settings);
VerifySpotlightAppears(ref settings);
Session.PerformMouseAction(MouseActionType.LeftClick);
}
[TestMethod("MouseUtils.FindMyMouse.DisableFindMyMouse2")]
[TestCategory("Mouse Utils #5")]
public void TestDisableFindMyMouse2()
{
LaunchFromSetting();
var settings = new FindMyMouseSettings();
settings.OverlayOpacity = "100";
settings.Radius = "50";
settings.InitialZoom = "1";
settings.AnimationDuration = "0";
settings.BackgroundColor = "000000";
settings.SpotlightColor = "FFFFFF";
var foundCustom = this.Find<Custom>("Find My Mouse");
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
// foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
SetFindMyMouseActivationMethod(ref foundCustom, "Press Left Control twice");
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
// SetFindMyMouseAppearanceBehavior(ref foundCustom, ref settings);
var excludedApps = foundCustom.Find<TextBlock>("Excluded apps");
if (excludedApps != null)
{
excludedApps.Click();
excludedApps.Click();
}
else
{
Assert.Fail("Activation method group not found.");
}
}
else
{
Assert.Fail("Find My Mouse group not found.");
}
// [Test Case]Enable FindMyMouse. Then, without moving your mouse: Press Left Ctrl twice and verify the overlay appears.
// VerifySpotlightSettings(ref settings);
// [Test Case] Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice
foundCustom.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
Task.Delay(2000).Wait();
Session.SendKey(Key.LCtrl, 0, 0);
Task.Delay(100).Wait();
Session.SendKey(Key.LCtrl, 0, 0);
VerifySpotlightDisappears(ref settings);
}
private void VerifySpotlightDisappears(ref FindMyMouseSettings settings)
{
Task.Delay(2000).Wait();
var location = Session.GetMousePosition();
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
var colorSpotlight = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreNotEqual("#" + settings.SpotlightColor, colorSpotlight);
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
Assert.AreNotEqual("#" + settings.BackgroundColor, colorBackground);
var colorBackground2 = this.GetPixelColorString(location.Item1 + radius + 100, location.Item2 + radius + 100);
Assert.AreNotEqual("#" + settings.BackgroundColor, colorBackground2);
}
private void VerifySpotlightAppears(ref FindMyMouseSettings settings)
{
Task.Delay(2000).Wait();
var location = Session.GetMousePosition();
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
var colorSpotlight = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreEqual("#" + settings.SpotlightColor, colorSpotlight);
var colorSpotlight2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
// Session.MoveMouseTo(location.Item1 + radius - 10, location.Item2);
Assert.AreEqual("#" + settings.SpotlightColor, colorSpotlight2);
Task.Delay(100).Wait();
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
Assert.AreEqual("#" + settings.BackgroundColor, colorBackground);
var colorBackground2 = this.GetPixelColorString(location.Item1 + radius + 100, location.Item2 + radius + 100);
Assert.AreEqual("#" + settings.BackgroundColor, colorBackground2);
}
private void ActivateSpotlight(ref FindMyMouseSettings settings)
{
var xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1 - 200, xy.Item2 - 100);
Task.Delay(1000).Wait();
Session.PerformMouseAction(MouseActionType.LeftClick);
Task.Delay(1000).Wait();
if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.PressLeftControlTwice)
{
Session.SendKey(Key.LCtrl, 0, 0);
Task.Delay(200).Wait();
Session.SendKey(Key.LCtrl, 0, 0);
}
else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.PressRightControlTwice)
{
Session.SendKey(Key.RCtrl, 0, 0);
Task.Delay(200).Wait();
Session.SendKey(Key.RCtrl, 0, 0);
}
else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.ShakeMouse)
{
// Simulate shake mouse;
}
else if (settings.SelectedActivationMethod == FindMyMouseSettings.ActivationMethod.CustomShortcut)
{
// Simulate custom shortcut
}
}
private void VerifySpotlightSettings(ref FindMyMouseSettings settings, bool equal = true)
{
ActivateSpotlight(ref settings);
VerifySpotlightAppears(ref settings);
}
private void SetFindMyMouseActivationMethod(ref Custom? foundCustom, string method)
{
Assert.IsNotNull(foundCustom);
var groupActivation = foundCustom.Find<TextBlock>("Activation method");
if (groupActivation != null)
{
groupActivation.Click();
string findMyMouseComboBoxKey = "Activation method";
var foundElements = foundCustom.FindAll<ComboBox>(findMyMouseComboBoxKey);
if (foundElements.Count != 0)
{
var myMouseComboBox = foundCustom.Find<ComboBox>(findMyMouseComboBoxKey);
Assert.IsNotNull(myMouseComboBox);
myMouseComboBox.Click();
var selectedItem = myMouseComboBox.Find<NavigationViewItem>(method);
Assert.IsNotNull(selectedItem);
selectedItem.Click();
}
else
{
Assert.IsTrue(false, "ComboBox is not found in the setting page.");
}
}
else
{
Assert.Fail("Activation method group not found.");
}
}
private void SetFindMyMouseAppearanceBehavior(ref Custom foundCustom, ref FindMyMouseSettings settings)
{
Assert.IsNotNull(foundCustom);
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
if (groupAppearanceBehavior != null)
{
// groupAppearanceBehavior.Click();
if (foundCustom.FindAll<Slider>("Overlay opacity (%)").Count == 0)
{
groupAppearanceBehavior.Click();
}
// Set the BackGround color
var backgroundColor = foundCustom.Find<Group>("Background color");
Assert.IsNotNull(backgroundColor);
var button = backgroundColor.Find<Button>(By.XPath(".//Button"));
Assert.IsNotNull(button);
button.Click();
var popupWindow = this.Find<Window>("Popup");
Assert.IsNotNull(popupWindow);
Task.Delay(1000).Wait();
var colorModelComboBox = this.Find<ComboBox>("Color model");
Assert.IsNotNull(colorModelComboBox);
colorModelComboBox.Click();
var selectedItem = colorModelComboBox.Find<NavigationViewItem>("RGB");
selectedItem.Click();
Task.Delay(500).Wait();
var rgbHexEdit = this.Find<TextBox>("RGB hex");
Assert.IsNotNull(rgbHexEdit);
Task.Delay(500).Wait();
int retry = 5;
while (retry > 0)
{
Task.Delay(500).Wait();
rgbHexEdit.SetText(settings.BackgroundColor);
Task.Delay(500).Wait();
string rgbHex = rgbHexEdit.Text;
bool isValid = rgbHex.StartsWith('#') && rgbHex.Length == 7 && rgbHex.Substring(1) == settings.BackgroundColor;
Task.Delay(500).Wait();
if (isValid)
{
break;
}
retry--;
}
button.Click();
// Set the Spotlight color
var spotlightColor = foundCustom.Find<Group>("Spotlight color");
Assert.IsNotNull(spotlightColor);
var spotlightColorButton = spotlightColor.Find<Button>(By.XPath(".//Button"));
Assert.IsNotNull(spotlightColorButton);
spotlightColorButton.Click();
var spotlightColorPopupWindow = Session.Find<Window>("Popup");
Assert.IsNotNull(spotlightColorPopupWindow);
var spotlightColorModelComboBox = this.Find<ComboBox>("Color model");
Assert.IsNotNull(spotlightColorModelComboBox);
spotlightColorModelComboBox.Click();
var selectedItem2 = spotlightColorModelComboBox.Find<NavigationViewItem>("RGB");
Assert.IsNotNull(selectedItem2);
selectedItem2.Click();
Task.Delay(500).Wait();
var rgbHexEdit2 = this.Find<TextBox>("RGB hex");
Assert.IsNotNull(rgbHexEdit2);
Task.Delay(500).Wait();
retry = 5;
while (retry > 0)
{
Task.Delay(500).Wait();
rgbHexEdit2.SetText(settings.SpotlightColor);
Task.Delay(500).Wait();
string rgbHex = rgbHexEdit2.Text;
bool isValid = rgbHex.StartsWith('#') && rgbHex.Length == 7 && rgbHex.Substring(1) == settings.SpotlightColor;
Task.Delay(500).Wait();
if (isValid)
{
break;
}
retry--;
}
Task.Delay(500).Wait();
spotlightColorButton.Click(false, 500, 1500);
// Set the overlay opacity to overlayOpacity%
var overlayOpacitySlider = foundCustom.Find<Slider>("Overlay opacity (%)");
Assert.IsNotNull(overlayOpacitySlider);
Assert.IsNotNull(settings.OverlayOpacity);
int overlayOpacityValue = int.Parse(settings.OverlayOpacity, CultureInfo.InvariantCulture);
overlayOpacitySlider.QuickSetValue(overlayOpacityValue);
Assert.AreEqual(settings.OverlayOpacity, overlayOpacitySlider.Text);
Task.Delay(1000).Wait();
// Set the Fade Initial zoom to 0
var spotlightInitialZoomSlider = foundCustom.Find<Slider>("Spotlight initial zoom");
Assert.IsNotNull(spotlightInitialZoomSlider);
Task.Delay(1000).Wait();
spotlightInitialZoomSlider.QuickSetValue(int.Parse(settings.InitialZoom, CultureInfo.InvariantCulture));
Assert.AreEqual(settings.InitialZoom, spotlightInitialZoomSlider.Text);
Task.Delay(1000).Wait();
//// Change the edit value
var spotlightRadiusEdit = foundCustom.Find<TextBox>("Spotlight radius (px) Minimum5");
Assert.IsNotNull(spotlightRadiusEdit);
Task.Delay(1000).Wait();
spotlightRadiusEdit.SetText(settings.Radius);
Assert.AreEqual(settings.Radius, spotlightRadiusEdit.Text);
Task.Delay(1000).Wait();
// Set the duration to 0 ms
var spotlightAnimationDuration = foundCustom.Find<TextBox>("Animation duration (ms) Minimum0");
Assert.IsNotNull(spotlightAnimationDuration);
Task.Delay(1000).Wait();
spotlightAnimationDuration.SetText(settings.AnimationDuration);
Assert.AreEqual(settings.AnimationDuration, spotlightAnimationDuration.Text);
Task.Delay(1000).Wait();
// groupAppearanceBehavior.Click();
}
else
{
Assert.Fail("Appearance & behavior group not found.");
}
}
private bool CheckAnimationEnable(ref Custom? foundCustom)
{
Assert.IsNotNull(foundCustom, "Find My Mouse group not found.");
var foundElements = foundCustom.FindAll<TextBlock>("Animations are disabled in your system settings.");
// Assert.IsNull(animationDisabledWarning);
if (foundElements.Count != 0)
{
var openSettingsLink = foundCustom.Find<Element>("Open settings");
Assert.IsNotNull(openSettingsLink);
openSettingsLink.Click(false, 500, 3000);
string settingsWindow = "Settings";
this.Session.Attach(settingsWindow);
var animationEffects = this.Find<ToggleSwitch>("Animation effects");
Assert.IsNotNull(animationEffects);
animationEffects.Toggle(true);
Task.Delay(2000).Wait();
Session.SendKeys(Key.Alt, Key.F4);
this.Session.Attach(PowerToysModule.PowerToysSettings);
this.LaunchFromSetting(reload: true);
}
else
{
return false;
}
return true;
}
private void LaunchFromSetting(bool reload = false, bool launchAsAdmin = false)
{
// this.Session.Attach(PowerToysModule.PowerToysSettings);
this.Session.SetMainWindowSize(WindowSize.Large);
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Mouse utilities", 10000).Count == 0)
{
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Input / Output").Click();
}
if (reload)
{
this.Find<NavigationViewItem>("Keyboard Manager").Click();
}
Task.Delay(1000).Wait();
this.Find<NavigationViewItem>("Mouse utilities").Click();
}
}
}

View File

@@ -1,498 +0,0 @@
// 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.Globalization;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Devices.Printers;
namespace MouseUtils.UITests
{
[TestClass]
public class MouseHighlighterTests : UITestBase
{
[TestMethod("MouseUtils.MouseHighlighter.EnableMouseHighlighter")]
[TestCategory("Mouse Utils #17")]
[TestCategory("Mouse Utils #18")]
[TestCategory("Mouse Utils #19")]
[TestCategory("Mouse Utils #20")]
[TestCategory("Mouse Utils #21")]
public void TestEnableMouseHighlighter()
{
LaunchFromSetting();
var foundCustom0 = this.Find<Custom>("Find My Mouse");
if (foundCustom0 != null)
{
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
}
else
{
Assert.Fail("Find My Mouse custom not found.");
}
var settings = new MouseHighlighterSettings();
settings.PrimaryButtonHighlightColor = "FFFF0000";
settings.SecondaryButtonHighlightColor = "FF00FF00";
settings.AlwaysHighlightColor = "004cFF71";
settings.Radius = "50";
settings.FadeDelay = "0";
settings.FadeDuration = "90";
var foundCustom = this.Find<Custom>("Mouse Highlighter");
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
var xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
// Change the shortcut key for MouseHighlighter
// [TestCase]Change activation shortcut and test it
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click();
Task.Delay(500).Wait();
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
// IOUtil.SimulateKeyPress(0x41);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
Session.SendKeys(Key.Win, Key.Shift, Key.H);
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1000);
SetMouseHighlighterAppearanceBehavior(ref foundCustom, ref settings);
var xy0 = Session.GetMousePosition();
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
Session.PerformMouseAction(MouseActionType.LeftClick);
// Check the mouse highlighter is enabled
Session.SendKeys(Key.Win, Key.Shift, Key.H);
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
Task.Delay(1000).Wait();
// MouseSimulator.LeftClick();
// [Test Case] Press the activation shortcut and press left and right click somewhere, verifying the highlights are applied.
// [Test Case] Press the activation shortcut again and verify no highlights appear when the mouse buttons are clicked.
VerifyMouseHighlighterAppears(ref settings, "leftClick");
VerifyMouseHighlighterAppears(ref settings, "rightClick");
// Disable mouse highlighter
Session.SendKeys(Key.Win, Key.Shift, Key.H);
Task.Delay(1000).Wait();
VerifyMouseHighlighterNotAppears(ref settings, "leftClick");
VerifyMouseHighlighterNotAppears(ref settings, "rightClick");
// [Test Case] Disable Mouse Highlighter and verify that the module is not activated when you press the activation shortcut.
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
Session.SendKeys(Key.Win, Key.Shift, Key.H);
Task.Delay(1000).Wait();
VerifyMouseHighlighterNotAppears(ref settings, "leftClick");
VerifyMouseHighlighterNotAppears(ref settings, "rightClick");
// [Test Case] With left mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
// [Test Case] With right mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
Session.SendKeys(Key.Win, Key.Shift, Key.H);
Task.Delay(1000).Wait();
VerifyMouseHighlighterDrag(ref settings, "leftClick");
VerifyMouseHighlighterDrag(ref settings, "rightClick");
}
else
{
Assert.Fail("Mouse Highlighter Custom not found.");
}
Task.Delay(500).Wait();
}
[TestMethod("MouseUtils.MouseHighlighter.MouseHighlighterDifferentSettings")]
[TestCategory("Mouse Utils #22")]
[TestCategory("Mouse Utils #23")]
[TestCategory("Mouse Utils #24")]
public void TestMouseHighlighterDifferentSettings()
{
LaunchFromSetting();
var foundCustom0 = this.Find<Custom>("Find My Mouse");
if (foundCustom0 != null)
{
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
}
else
{
Assert.Fail("Find My Mouse custom not found.");
}
var settings = new MouseHighlighterSettings();
settings.PrimaryButtonHighlightColor = "FF000000";
settings.SecondaryButtonHighlightColor = "FFFFFFFF";
settings.AlwaysHighlightColor = "004cFF71";
settings.Radius = "70";
settings.FadeDelay = "0";
settings.FadeDuration = "90";
var foundCustom = this.Find<Custom>("Mouse Highlighter");
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
var xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
// Change the shortcut key for MouseHighlighter
// [TestCase] Test the different settings and verify they apply - Change activation shortcut and test it
// [Test Case] Test the different settings and verify they apply - Left button highlight color
// [Test Case] Test the different settings and verify they apply - Right button highlight color
// [Test Case] Test the different settings and verify they apply - Radius
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click();
Task.Delay(500).Wait();
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
// IOUtil.SimulateKeyPress(0x41);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
Session.SendKeys(Key.Win, Key.Shift, Key.O);
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1000);
SetMouseHighlighterAppearanceBehavior(ref foundCustom, ref settings);
var xy0 = Session.GetMousePosition();
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
Session.PerformMouseAction(MouseActionType.LeftClick);
// Check the mouse highlighter is enabled
Session.SendKeys(Key.Win, Key.Shift, Key.O);
Task.Delay(1000).Wait();
VerifyMouseHighlighterAppears(ref settings, "leftClick");
VerifyMouseHighlighterAppears(ref settings, "rightClick");
}
else
{
Assert.Fail("Mouse Highlighter Custom not found.");
}
Task.Delay(500).Wait();
}
private void VerifyMouseHighlighterDrag(ref MouseHighlighterSettings settings, string action = "leftClick")
{
Task.Delay(500).Wait();
string expectedColor = string.Empty;
if (action == "leftClick")
{
IOUtil.SimulateMouseDown(true);
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
}
else if (action == "rightClick")
{
IOUtil.SimulateMouseDown(false);
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
}
else
{
Assert.Fail("Invalid action specified.");
}
expectedColor = "#" + expectedColor;
Task.Delay(100).Wait();
var location = Session.GetMousePosition();
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick);
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick2);
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
Assert.AreNotEqual(expectedColor, colorBackground);
// Drag the mouse
// Session.MoveMouseTo(location.Item1 - 400, location.Item2);
for (int i = 0; i < 500; i++)
{
IOUtil.MoveMouseBy(-1, 0);
Task.Delay(10).Wait();
}
Task.Delay(2000).Wait();
location = Session.GetMousePosition();
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick);
colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick2);
colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
Assert.AreNotEqual(expectedColor, colorBackground);
if (action == "leftClick")
{
IOUtil.SimulateMouseUp(true);
}
else if (action == "rightClick")
{
IOUtil.SimulateMouseUp(false);
}
int duration = int.Parse(settings.FadeDuration, CultureInfo.InvariantCulture);
Task.Delay(duration + 100).Wait();
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreNotEqual("#" + settings.PrimaryButtonHighlightColor, colorLeftClick);
}
private void VerifyMouseHighlighterNotAppears(ref MouseHighlighterSettings settings, string action = "leftClick")
{
Task.Delay(500).Wait();
string expectedColor = string.Empty;
if (action == "leftClick")
{
// MouseSimulator.LeftDown();
Session.PerformMouseAction(MouseActionType.LeftDown);
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
}
else if (action == "rightClick")
{
// MouseSimulator.RightDown();
Session.PerformMouseAction(MouseActionType.RightDown);
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
}
else
{
Assert.Fail("Invalid action specified.");
}
expectedColor = "#" + expectedColor;
var location = Session.GetMousePosition();
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreNotEqual(expectedColor, colorLeftClick);
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
Assert.AreNotEqual(expectedColor, colorLeftClick2);
if (action == "leftClick")
{
Session.PerformMouseAction(MouseActionType.LeftUp);
}
else if (action == "rightClick")
{
Session.PerformMouseAction(MouseActionType.RightUp);
}
}
private void VerifyMouseHighlighterAppears(ref MouseHighlighterSettings settings, string action = "leftClick")
{
Task.Delay(500).Wait();
string expectedColor = string.Empty;
if (action == "leftClick")
{
// MouseSimulator.LeftDown();
Session.PerformMouseAction(MouseActionType.LeftDown);
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
}
else if (action == "rightClick")
{
// MouseSimulator.RightDown();
Session.PerformMouseAction(MouseActionType.RightDown);
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
}
else
{
Assert.Fail("Invalid action specified.");
}
expectedColor = "#" + expectedColor;
var location = Session.GetMousePosition();
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick);
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
Assert.AreEqual(expectedColor, colorLeftClick2);
Task.Delay(500).Wait();
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
Assert.AreNotEqual(expectedColor, colorBackground);
if (action == "leftClick")
{
// MouseSimulator.LeftUp();
Session.PerformMouseAction(MouseActionType.LeftUp);
}
else if (action == "rightClick")
{
// MouseSimulator.RightUp();
Session.PerformMouseAction(MouseActionType.RightUp);
}
int duration = int.Parse(settings.FadeDuration, CultureInfo.InvariantCulture);
Task.Delay(duration + 100).Wait();
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreNotEqual("#" + settings.PrimaryButtonHighlightColor, colorLeftClick);
}
private void SetColor(ref Custom foundCustom, string colorName = "Primary button highlight color", string colorValue = "000000", string opacity = "0")
{
Assert.IsNotNull(foundCustom);
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
if (groupAppearanceBehavior != null)
{
if (foundCustom.FindAll<TextBox>("Fade duration (ms) Minimum0").Count == 0)
{
groupAppearanceBehavior.Click();
}
// Set primary button highlight color
var primaryButtonHighlightColor = foundCustom.Find<Group>(colorName);
Assert.IsNotNull(primaryButtonHighlightColor);
var button = primaryButtonHighlightColor.Find<Button>(By.XPath(".//Button"));
Assert.IsNotNull(button);
button.Click(false, 500, 700);
var popupWindow = Session.Find<Window>("Popup");
Assert.IsNotNull(popupWindow);
var colorModelComboBox = this.Find<ComboBox>("Color model");
Assert.IsNotNull(colorModelComboBox);
colorModelComboBox.Click(false, 500, 700);
var selectedItem = colorModelComboBox.Find<NavigationViewItem>("RGB");
selectedItem.Click();
var rgbHexEdit = this.Find<TextBox>("RGB hex");
Assert.IsNotNull(rgbHexEdit);
Task.Delay(500).Wait();
rgbHexEdit.SetText(colorValue);
int retry = 5;
while (retry > 0)
{
Task.Delay(500).Wait();
rgbHexEdit.SetText(colorValue);
Task.Delay(500).Wait();
string rgbHex = rgbHexEdit.Text;
bool isValid = rgbHex.StartsWith('#') && rgbHex.Length == 9 && rgbHex.Substring(1) == colorValue;
Task.Delay(500).Wait();
if (isValid)
{
break;
}
retry--;
}
Task.Delay(500).Wait();
button.Click();
}
}
private void SetMouseHighlighterAppearanceBehavior(ref Custom foundCustom, ref MouseHighlighterSettings settings)
{
Assert.IsNotNull(foundCustom);
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
if (groupAppearanceBehavior != null)
{
// groupAppearanceBehavior.Click();
if (foundCustom.FindAll<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDurationEdit)).Count == 0)
{
groupAppearanceBehavior.Click();
}
// Set primary button highlight color
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.PrimaryButtonHighlightColorGroup), settings.PrimaryButtonHighlightColor);
// Set secondary button highlight color
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.SecondaryButtonHighlightColorGroup), settings.SecondaryButtonHighlightColor);
// Set the duration to duration ms
var fadeDurationEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDurationEdit));
Assert.IsNotNull(fadeDurationEdit);
fadeDurationEdit.SetText(settings.FadeDuration);
Assert.AreEqual(settings.FadeDuration, fadeDurationEdit.Text);
// Set Fade delay(ms)
var fadeDelayEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDelayEdit));
Assert.IsNotNull(fadeDelayEdit);
fadeDelayEdit.SetText(settings.FadeDelay);
Assert.AreEqual(settings.FadeDelay, fadeDelayEdit.Text);
// Set the fade radius (px)
var fadeRadiusEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.RadiusEdit));
Assert.IsNotNull(fadeRadiusEdit);
fadeRadiusEdit.SetText(settings.Radius);
Assert.AreEqual(settings.Radius, fadeRadiusEdit.Text);
// Set always highlight color
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.AlwaysHighlightColorGroup), settings.AlwaysHighlightColor);
}
else
{
Assert.Fail("Appearance & behavior group not found.");
}
}
private void LaunchFromSetting(bool showWarning = false, bool launchAsAdmin = false)
{
this.Session.SetMainWindowSize(WindowSize.Large);
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
{
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Input / Output").Click();
}
this.Find<NavigationViewItem>("Mouse utilities").Click();
}
}
}

View File

@@ -1,250 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MouseUtils.UITests
{
[TestClass]
public class MouseJumpTests : UITestBase
{
[TestMethod("MouseUtils.MouseJump.EnableMouseJump")]
[TestCategory("Mouse Utils #39")]
[TestCategory("Mouse Utils #40")]
[TestCategory("Mouse Utils #41")]
[TestCategory("Mouse Utils #45")]
public void TestEnableMouseJump()
{
LaunchFromSetting(true);
}
[TestMethod("MouseUtils.MouseJump.EnableMouseJump2")]
[TestCategory("Mouse Utils #39")]
[TestCategory("Mouse Utils #41")]
[TestCategory("Mouse Utils #45")]
public void TestEnableMouseJump2()
{
LaunchFromSetting();
var foundCustom0 = this.Find<Custom>("Find My Mouse");
if (foundCustom0 != null)
{
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
}
else
{
Assert.Fail("Find My Mouse custom not found.");
}
for (int i = 0; i < 10; i++)
{
Session.PerformMouseAction(MouseActionType.ScrollDown);
}
var foundCustom = this.Find<Custom>("Mouse Jump");
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
var xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
// Change the shortcut key for MouseHighlighter
// [TestCase]Change activation shortcut and test it
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click(false, 500, 1000);
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
// IOUtil.SimulateKeyPress(0x41);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1500);
var screenCenter = this.GetScreenCenter();
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY, 500, 1000);
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
// [TestCase] Enable Mouse Jump. Then - Press the activation shortcut and verify the screens preview appears.
// [TestCase] Enable Mouse Jump. Then - Click around the screen preview and ensure that mouse cursor jumped to clicked location.
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
VerifyWindowAppears();
Task.Delay(1000).Wait();
// [TestCase] Enable Mouse Jump. Then - Disable Mouse Jump and verify that the module is not activated when you press the activation shortcut.
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(false);
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
Task.Delay(500).Wait();
VerifyWindowNotAppears();
}
else
{
Assert.Fail("Mouse Highlighter Custom not found.");
}
Task.Delay(500).Wait();
}
[TestMethod("MouseUtils.MouseJump.EnableMouseJump3")]
[TestCategory("Mouse Utils #40")]
public void TestEnableMouseJump3()
{
LaunchFromSetting();
var foundCustom0 = this.Find<Custom>("Find My Mouse");
if (foundCustom0 != null)
{
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
}
else
{
Assert.Fail("Find My Mouse custom not found.");
}
for (int i = 0; i < 10; i++)
{
Session.PerformMouseAction(MouseActionType.ScrollDown);
}
var foundCustom = this.Find<Custom>("Mouse Jump");
if (foundCustom != null)
{
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
var xy = Session.GetMousePosition();
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
// Change the shortcut key for MouseHighlighter
// [TestCase]Change activation shortcut and test it
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click(false, 500, 1000);
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
// IOUtil.SimulateKeyPress(0x41);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
Session.SendKeys(Key.Win, Key.Shift, Key.J);
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1500);
var screenCenter = this.GetScreenCenter();
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY, 500, 1000);
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
// [TestCase] Enable Mouse Jump. Then - Change activation shortcut and verify that new shortcut triggers Mouse Jump.
Session.SendKeys(Key.Win, Key.Shift, Key.J);
VerifyWindowAppears();
}
else
{
Assert.Fail("Mouse Highlighter Custom not found.");
}
Task.Delay(500).Wait();
}
private void VerifyWindowAppears()
{
string windowName = "MouseJump";
Session.Attach(windowName);
var center = this.Session.GetMainWindowCenter();
Session.MoveMouseTo(center.CenterX, center.CenterY);
Session.PerformMouseAction(MouseActionType.LeftClick, 1000, 1000);
var screenCenter = this.GetScreenCenter();
// Get Mouse position
var xy = Session.GetMousePosition();
double distance = CalculateDistance(xy.Item1, xy.Item2, screenCenter.CenterX, screenCenter.CenterY);
Assert.IsTrue(distance <= 10, "Mouse Jump window should be opened and mouse should be moved to the center of the screen.");
}
private void VerifyWindowNotAppears()
{
string windowName = "MouseJump";
bool open = this.IsWindowOpen(windowName);
Assert.IsFalse(open, "Mouse Jump window should not be opened.");
}
/// <summary>
/// Calculate the Euclidean distance between two 2D points
/// </summary>
/// <param name="x1">X coordinate of first point</param>
/// <param name="y1">Y coordinate of first point</param>
/// <param name="x2">X coordinate of second point</param>
/// <param name="y2">Y coordinate of second point</param>
/// <returns>Distance (double)</returns>
public double CalculateDistance(int x1, int y1, int x2, int y2)
{
int dx = x2 - x1;
int dy = y2 - y1;
return Math.Sqrt((dx * dx) + (dy * dy));
}
private void LaunchFromSetting(bool firstTime = false, bool launchAsAdmin = false)
{
Session.SetMainWindowSize(WindowSize.Large);
Task.Delay(1000).Wait();
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
{
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
Task.Delay(2000).Wait();
}
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
{
RestartScopeExe();
Session.SetMainWindowSize(WindowSize.Large);
Task.Delay(1000).Wait();
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
Task.Delay(2000).Wait();
}
// Click on the Mouse utilities
// Task.Delay(2000).Wait();
if (firstTime)
{
return;
}
else
{
this.Find<NavigationViewItem>("Mouse utilities").Click();
}
}
}
}

View File

@@ -1,404 +0,0 @@
// 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.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Devices.Printers;
namespace MouseUtils.UITests
{
[TestClass]
public class MousePointerCrosshairsTests : UITestBase
{
[TestMethod("MouseUtils.MousePointerCrosshairs.EnableMousePointerCrosshairs")]
[TestCategory("Mouse Utils #29")]
[TestCategory("Mouse Utils #30")]
[TestCategory("Mouse Utils #31")]
public void TestEnableMousePointerCrosshairs()
{
LaunchFromSetting();
var settings = new MousePointerCrosshairsSettings();
settings.CrosshairsColor = "FF0000";
settings.CrosshairsBorderColor = "FF0000";
settings.Opacity = "100";
settings.CenterRadius = "0";
settings.Thickness = "20";
settings.BorderSize = "0";
settings.IsFixLength = false;
settings.FixedLength = "1";
var foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.FindMyMouse);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.FindMyMouse, false);
foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.MouseHighlighter);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MouseHighlighter, true);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MouseHighlighter, false);
for (int i = 0; i < 10; i++)
{
Session.PerformMouseAction(MouseActionType.ScrollDown);
}
foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.MousePointerCrosshairs);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MousePointerCrosshairs, false);
Task.Delay(500).Wait();
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MousePointerCrosshairs, true);
Assert.IsNotNull(foundCustom);
// [Test Case] Change activation shortcut and test it.
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click(false, 500, 1000);
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
Session.SendKeys(Key.Win, Key.Alt, Key.A);
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1000);
SetMousePointerCrosshairsAppearanceBehavior(ref foundCustom, ref settings);
Task.Delay(500).Wait();
// [Test Case] Press the activation shortcut and verify the crosshairs appear, and that they follow the mouse around.
var xy0 = Session.GetMousePosition();
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
IOUtil.MouseClick();
Task.Delay(500).Wait();
Session.SendKeys(Key.Win, Key.Alt, Key.A);
Task.Delay(1000).Wait();
xy0 = Session.GetMousePosition();
VerifyMousePointerCrosshairsAppears(ref settings);
Task.Delay(500).Wait();
for (int i = 0; i < 100; i++)
{
IOUtil.MoveMouseBy(-1, 0);
Task.Delay(10).Wait();
}
VerifyMousePointerCrosshairsAppears(ref settings);
// [Test Case] Press the activation shortcut again and verify the crosshairs disappear.
Session.SendKeys(Key.Win, Key.Alt, Key.A);
Task.Delay(1000).Wait();
VerifyMousePointerCrosshairsNotAppears(ref settings);
Task.Delay(500).Wait();
// [Test Case] Disable Mouse Pointer Crosshairs and verify that the module is not activated when you press the activation shortcut.
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MousePointerCrosshairs, false);
xy0 = Session.GetMousePosition();
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
Session.PerformMouseAction(MouseActionType.LeftClick);
Session.SendKeys(Key.Win, Key.Alt, Key.A);
Task.Delay(1000).Wait();
VerifyMousePointerCrosshairsNotAppears(ref settings);
}
[TestMethod("MouseUtils.MousePointerCrosshairs.MousePointerCrosshairsDifferentSettings")]
[TestCategory("Mouse Utils #32")]
[TestCategory("Mouse Utils #33")]
public void TestMousePointerCrosshairsDifferentSettings()
{
LaunchFromSetting();
var settings = new MousePointerCrosshairsSettings();
settings.CrosshairsColor = "00FF00";
settings.CrosshairsBorderColor = "00FF00";
settings.Opacity = "100";
settings.CenterRadius = "0";
settings.Thickness = "20";
settings.BorderSize = "0";
settings.IsFixLength = false;
settings.FixedLength = "1";
var foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.FindMyMouse);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.FindMyMouse, false);
foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.MouseHighlighter);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MouseHighlighter, true);
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MouseHighlighter, false);
for (int i = 0; i < 10; i++)
{
Session.PerformMouseAction(MouseActionType.ScrollDown);
}
foundCustom = FindMouseUtilElement(MouseUtilsSettings.MouseUtils.MousePointerCrosshairs);
// this.FindGroup("Enable Mouse Pointer Crosshairs");
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MousePointerCrosshairs, false);
Task.Delay(500).Wait();
MouseUtilsSettings.SetMouseUtilEnabled(foundCustom, MouseUtilsSettings.MouseUtils.MousePointerCrosshairs, true);
Assert.IsNotNull(foundCustom);
// [Test Case] Change activation shortcut and test it.
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
Assert.IsNotNull(activationShortcutButton);
activationShortcutButton.Click(false, 500, 1000);
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
Assert.IsNotNull(activationShortcutWindow);
// Invalid shortcut key
Session.SendKeySequence(Key.H);
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
Assert.IsNotNull(invalidShortcutText);
Session.SendKeys(Key.Win, Key.Alt, Key.P);
var saveButton = activationShortcutWindow.Find<Button>("Save");
Assert.IsNotNull(saveButton);
saveButton.Click(false, 500, 1000);
SetMousePointerCrosshairsAppearanceBehavior(ref foundCustom, ref settings);
Task.Delay(500).Wait();
// [Test Case] Test the different settings and verify they apply - Change activation shortcut and test it.
// [Test Case] Test the different settings and verify they apply - Crosshairs color.
var xy0 = Session.GetMousePosition();
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
IOUtil.MouseClick();
Task.Delay(500).Wait();
Session.SendKeys(Key.Win, Key.Alt, Key.P);
Task.Delay(1000).Wait();
xy0 = Session.GetMousePosition();
VerifyMousePointerCrosshairsAppears(ref settings);
Task.Delay(500).Wait();
for (int i = 0; i < 100; i++)
{
IOUtil.MoveMouseBy(-1, 0);
Task.Delay(10).Wait();
}
VerifyMousePointerCrosshairsAppears(ref settings);
// Press the activation shortcut again and verify the crosshairs disappear.
Session.SendKeys(Key.Win, Key.Alt, Key.P);
Task.Delay(1000).Wait();
VerifyMousePointerCrosshairsNotAppears(ref settings);
}
private void VerifyMousePointerCrosshairsNotAppears(ref MousePointerCrosshairsSettings settings)
{
Task.Delay(500).Wait();
string expectedColor = string.Empty;
expectedColor = "#" + settings.CrosshairsColor;
var location = Session.GetMousePosition();
int radius = int.Parse(settings.CenterRadius, CultureInfo.InvariantCulture);
var color = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreNotEqual(expectedColor, color);
}
private void VerifyMousePointerCrosshairsAppears(ref MousePointerCrosshairsSettings settings)
{
Task.Delay(1000).Wait();
string expectedColor = string.Empty;
expectedColor = "#" + settings.CrosshairsColor;
var location = Session.GetMousePosition();
int radius = int.Parse(settings.CenterRadius, CultureInfo.InvariantCulture);
var color = this.GetPixelColorString(location.Item1, location.Item2);
Assert.AreEqual(expectedColor, color, "Center color check failed");
var colorX = this.GetPixelColorString(location.Item1 + 50, location.Item2);
Assert.AreEqual(expectedColor, colorX, "Center x + 50 color check failed");
colorX = this.GetPixelColorString(location.Item1 - 50, location.Item2);
Assert.AreEqual(expectedColor, colorX, "Center x - 50 color check failed");
var colorY = this.GetPixelColorString(location.Item1, location.Item2 + 50);
Assert.AreEqual(expectedColor, colorY, "Center y + 50 color check failed");
colorY = this.GetPixelColorString(location.Item1, location.Item2 - 50);
Assert.AreEqual(expectedColor, colorY, "Center y + 50 color check failed");
}
private void SetColor(ref Custom foundCustom, string colorName, string colorValue = "000000")
{
Assert.IsNotNull(foundCustom);
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
if (groupAppearanceBehavior != null)
{
// Set primary button highlight color
var primaryButtonHighlightColor = foundCustom.Find<Group>(colorName);
Assert.IsNotNull(primaryButtonHighlightColor);
var button = primaryButtonHighlightColor.Find<Button>(By.XPath(".//Button"));
Assert.IsNotNull(button);
button.Click(false);
var popupWindow = Session.Find<Window>("Popup");
Assert.IsNotNull(popupWindow);
var colorModelComboBox = this.Find<ComboBox>("Color model");
Assert.IsNotNull(colorModelComboBox);
colorModelComboBox.Click();
var selectedItem = colorModelComboBox.Find<NavigationViewItem>("RGB");
selectedItem.Click();
var rgbHexEdit = this.Find<TextBox>("RGB hex");
Assert.IsNotNull(rgbHexEdit);
rgbHexEdit.SetText(colorValue);
button.Click();
}
}
private void SetMousePointerCrosshairsAppearanceBehavior(ref Custom foundCustom, ref MousePointerCrosshairsSettings settings)
{
Assert.IsNotNull(foundCustom);
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
if (groupAppearanceBehavior != null)
{
// groupAppearanceBehavior.Click();
if (foundCustom.FindAll<TextBox>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.ThicknessEdit)).Count == 0)
{
groupAppearanceBehavior.Click();
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
Session.PerformMouseAction(MouseActionType.ScrollDown);
}
// Set the crosshairs color
SetColor(ref foundCustom, settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.CrosshairsColorGroup), settings.CrosshairsColor);
// Set the crosshairs border color
SetColor(ref foundCustom, settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.CrosshairsBorderColorGroup), settings.CrosshairsBorderColor);
// Set the duration to duration ms
var opacitySlider = foundCustom.Find<Slider>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.OpacitySlider));
Assert.IsNotNull(opacitySlider);
Assert.IsNotNull(settings.Opacity);
int opacityValue = int.Parse(settings.Opacity, CultureInfo.InvariantCulture);
opacitySlider.QuickSetValue(opacityValue);
Assert.AreEqual(settings.Opacity, opacitySlider.Text);
Task.Delay(500).Wait();
// Set the center radius (px)
var centerRadiusEdit = foundCustom.Find<TextBox>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.CenterRadiusEdit));
Assert.IsNotNull(centerRadiusEdit);
centerRadiusEdit.SetText(settings.CenterRadius);
Assert.AreEqual(settings.CenterRadius, centerRadiusEdit.Text);
// Set the thickness (px)
var thicknessEdit = foundCustom.Find<TextBox>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.ThicknessEdit));
Assert.IsNotNull(thicknessEdit);
thicknessEdit.SetText(settings.Thickness);
Assert.AreEqual(settings.Thickness, thicknessEdit.Text);
// Set the border size (px)
var borderSizeEdit = foundCustom.Find<TextBox>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.BorderSizeEdit));
Assert.IsNotNull(borderSizeEdit);
borderSizeEdit.SetText(settings.BorderSize);
Assert.AreEqual(settings.BorderSize, borderSizeEdit.Text);
// Set the fixed length (px)
var isFixedLength = foundCustom.Find<ToggleSwitch>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.IsFixLengthToggle));
Assert.IsNotNull(isFixedLength);
isFixedLength.Toggle(settings.IsFixLength);
Assert.AreEqual(settings.IsFixLength, isFixedLength.IsOn);
if (settings.IsFixLength)
{
var fixedLengthEdit = foundCustom.Find<TextBox>(settings.GetElementName(MousePointerCrosshairsSettings.SettingsUIElements.FixedLengthEdit));
Assert.IsNotNull(fixedLengthEdit);
fixedLengthEdit.SetText(settings.FixedLength);
Assert.AreEqual(settings.FixedLength, fixedLengthEdit.Text);
}
}
else
{
Assert.Fail("Appearance & behavior group not found.");
}
}
private bool FindGroup(string groupName)
{
try
{
var foundElements = this.FindAll<Element>(groupName);
foreach (var element in foundElements)
{
string className = element.ClassName;
string name = element.Name;
string text = element.Text;
string helptext = element.HelpText;
string controlType = element.ControlType;
}
if (foundElements.Count == 0)
{
return false;
}
}
catch (Exception ex)
{
// Validate if group is not found by checking exception.Message
return ex.Message.Contains("No element found");
}
return true;
}
public Custom? FindMouseUtilElement(MouseUtilsSettings.MouseUtils element)
{
var elementName = MouseUtilsSettings.GetMouseUtilUIName(element);
var foundCustom = this.Find<Custom>(elementName);
for (int i = 0; i < 20; i++)
{
if (foundCustom != null)
{
break;
}
Session.PerformMouseAction(MouseActionType.ScrollDown);
foundCustom = this.Find<Custom>(elementName);
}
return foundCustom;
}
private void LaunchFromSetting(bool showWarning = false, bool launchAsAdmin = false)
{
Session.SetMainWindowSize(WindowSize.Large);
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
{
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Input / Output").Click();
}
this.Find<NavigationViewItem>("Mouse utilities").Click();
}
}
}

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}</ProjectGuid>
<RootNamespace>PowerToys.MouseUtils.UITests</RootNamespace>
<AssemblyName>PowerToys.MouseUtils.UITests</AssemblyName>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\MouseUtils.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,64 +0,0 @@
## [Mouse Utils](tests-checklist-template-mouse-utils-section.md)
Find My Mouse:
* Enable FindMyMouse. Then, without moving your mouse:
- [x] Press Left Ctrl twice and verify the overlay appears.
- [x] Press any other key and verify the overlay disappears.
- [x] Press Left Ctrl twice and verify the overlay appears.
- [x] Press a mouse button and verify the overlay disappears.
* Disable FindMyMouse. Verify the overlay no longer appears when you press Left Ctrl twice.
* Enable FindMyMouse. Then, without moving your mouse:
- [x] Press Left Ctrl twice and verify the overlay appears.
* Enable the "Do not activate on game mode" option. Start playing a game that uses CG native full screen.
- [ ] Verify the overlay no longer appears when you press Left Ctrl twice.
* Disable the "Do not activate on game mode" option. Start playing the same game.
- [ ] Verify the overlay appears when you press Left Ctrl twice. (though it'll likely minimize the game)
* Test the different settings and verify they apply:
- [ ] Overlay opacity
- [x] Background color
- [x] Spotlight color
- [x] Spotlight radius
- [ ] Spotlight initial zoom (1x vs 9x will show the difference)
- [ ] Animation duration
- [ ] Change activation method to shake and activate by shaking your mouse pointer
- [ ] Excluded apps
Mouse Highlighter:
* Enable Mouse Highlighter. Then:
- [x] Press the activation shortcut and press left and right click somewhere, verifying the hightlights are applied.
- [x] With left mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
- [x] With right mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
- [x] Press the activation shortcut again and verify no highlights appear when the mouse buttons are clicked.
- [x] Disable Mouse Highlighter and verify that the module is not activated when you press the activation shortcut.
* Test the different settings and verify they apply:
- [x] Change activation shortcut and test it
- [x] Left button highlight color
- [x] Right button highlight color
- [ ] Opacity
- [ ] Radius
- [ ] Fade delay
- [ ] Fade duration
Mouse Pointer Crosshairs:
* Enable Mouse Pointer Crosshairs. Then:
- [x] Press the activation shortcut and verify the crosshairs appear, and that they follow the mouse around.
- [x] Press the activation shortcut again and verify the crosshairs disappear.
- [x] Disable Mouse Pointer Crosshairs and verify that the module is not activated when you press the activation shortcut.
* Test the different settings and verify they apply:
- [x] Change activation shortcut and test it
- [x] Crosshairs color
- [ ] Crosshairs opacity
- [ ] Crosshairs center radius
- [ ] Crosshairs thickness
- [ ] Crosshairs border color
- [ ] Crosshairs border size
Mouse Jump:
* Enable Mouse Jump. Then:
- [x] Press the activation shortcut and verify the screens preview appears.
- [x] Change activation shortcut and verify that new shortcut triggers Mouse Jump.
- [x] Click around the screen preview and ensure that mouse cursor jumped to clicked location.
- [ ] Reorder screens in Display settings and confirm that Mouse Jump reflects the change and still works correctly.
- [ ] Change scaling of screens and confirm that Mouse Jump still works correctly.
- [ ] Unplug additional monitors and confirm that Mouse Jump still works correctly.
- [x] Disable Mouse Jump and verify that the module is not activated when you press the activation shortcut.

View File

@@ -1,58 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MouseUtils.UITests
{
public class FindMyMouseSettings
{
// Appearance settings
public string OverlayOpacity { get; set; }
public string Radius { get; set; }
public string InitialZoom { get; set; }
public string AnimationDuration { get; set; }
// Color settings
public string BackgroundColor { get; set; }
public string SpotlightColor { get; set; }
// Activation method settings
public enum ActivationMethod
{
PressLeftControlTwice,
PressRightControlTwice,
ShakeMouse,
CustomShortcut,
}
public ActivationMethod SelectedActivationMethod { get; set; }
// Optional constructor to initialize properties
public FindMyMouseSettings(
string overlayOpacity = "",
string radius = "",
string initialZoom = "",
string animationDuration = "",
string backgroundColor = "",
string spotlightColor = "")
{
OverlayOpacity = overlayOpacity;
Radius = radius;
InitialZoom = initialZoom;
AnimationDuration = animationDuration;
BackgroundColor = backgroundColor;
SpotlightColor = spotlightColor;
SelectedActivationMethod = ActivationMethod.PressLeftControlTwice; // Default value
}
}
}

View File

@@ -1,291 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
// The MouseUtils module relies on simulating system-level input events (such as mouse movement or key presses) to test visual or behavioral responses.
// The UI Test framework provides built-in methods for simulating mouse movement and clicks, which work for MouseUtils reliably on high-performance dev boxes.
// However, on low-performance environments such as CI/CD pipelines or virtual machines, these simulated input events are not always correctly recognized by the operating system.
// IOUtils class is added specifically for MouseUtils tests.
// For any test scenario that involves simulating continuous mouse movement (e.g., detecting crosshair changes while moving the cursor),
// input simulation methods in IOUtils class should be used.
namespace MouseUtils.UITests
{
public class IOUtil
{
private readonly UIntPtr ignoreKeyEventFlag = 0x5555;
[DllImport("user32.dll")]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct INPUT
{
internal INPUTTYPE type;
internal InputUnion data;
internal static int Size
{
get { return Marshal.SizeOf<INPUT>(); }
}
}
[StructLayout(LayoutKind.Explicit)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal uint dwFlags;
internal uint time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct KEYBDINPUT
{
internal short wVk;
internal short wScan;
internal uint dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct HARDWAREINPUT
{
internal int uMsg;
internal short wParamL;
internal short wParamH;
}
internal enum INPUTTYPE : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2,
}
[Flags]
internal enum KeyEventF
{
KeyDown = 0x0000,
ExtendedKey = 0x0001,
KeyUp = 0x0002,
Unicode = 0x0004,
Scancode = 0x0008,
}
[Flags]
internal enum MouseEventF : uint
{
MOVE = 0x0001,
LEFTDOWN = 0x0002,
LEFTUP = 0x0004,
RIGHTDOWN = 0x0008,
RIGHTUP = 0x0010,
ABSOLUTE = 0x8000,
MIDDLEDOWN = 0x0020,
MIDDLEUP = 0x0040,
}
public static void SimulateMouseDown(bool leftButton = true)
{
SendMouseInput(leftButton ? MouseEventF.LEFTDOWN : MouseEventF.RIGHTDOWN);
}
public static void SimulateMouseUp(bool leftButton = true)
{
SendMouseInput(leftButton ? MouseEventF.LEFTUP : MouseEventF.RIGHTUP);
}
public static void MouseClick(bool leftButton = true)
{
SendMouseInput(leftButton ? MouseEventF.LEFTDOWN : MouseEventF.RIGHTDOWN);
SendMouseInput(leftButton ? MouseEventF.LEFTUP : MouseEventF.RIGHTUP);
}
private static void SendMouseInput(MouseEventF mouseFlags)
{
var input = new INPUT
{
type = INPUTTYPE.INPUT_MOUSE,
data = new InputUnion
{
mi = new MOUSEINPUT
{
dx = 0,
dy = 0,
mouseData = 0,
dwFlags = (uint)mouseFlags,
time = 0,
dwExtraInfo = UIntPtr.Zero,
},
},
};
INPUT[] inputs = [input];
_ = SendInput(1, inputs, INPUT.Size);
}
[DllImport("user32.dll")]
private static extern IntPtr GetMessageExtraInfo();
public static void MoveMouseBy(int dx, int dy)
{
var input = new INPUT
{
type = INPUTTYPE.INPUT_MOUSE,
data = new InputUnion
{
mi = new MOUSEINPUT
{
dx = dx,
dy = dy,
mouseData = 0,
dwFlags = (uint)MouseEventF.MOVE,
time = 0,
dwExtraInfo = (nuint)GetMessageExtraInfo(),
},
},
};
INPUT[] inputs = [input];
_ = SendInput(1, inputs, INPUT.Size);
}
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
public static void MoveMouseTo(int x, int y)
{
int screenWidth = GetSystemMetrics(0);
int screenHeight = GetSystemMetrics(1);
int normalizedX = (int)(x * 65535 / screenWidth);
int normalizedY = (int)(y * 65535 / screenHeight);
var input = new INPUT
{
type = INPUTTYPE.INPUT_MOUSE,
data = new InputUnion
{
mi = new MOUSEINPUT
{
dx = normalizedX,
dy = normalizedY,
mouseData = 0,
dwFlags = (uint)(MouseEventF.MOVE | MouseEventF.ABSOLUTE),
time = 0,
dwExtraInfo = UIntPtr.Zero,
},
},
};
INPUT[] inputs = [input];
_ = SendInput(1, inputs, INPUT.Size);
}
private void SendSingleKeyboardInput(short keyCode, uint keyStatus)
{
var inputShift = new INPUT
{
type = INPUTTYPE.INPUT_KEYBOARD,
data = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = keyCode,
dwFlags = keyStatus,
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
dwExtraInfo = ignoreKeyEventFlag,
},
},
};
INPUT[] inputs = [inputShift];
_ = SendInput(1, inputs, INPUT.Size);
}
public static void SimulateKeyDown(ushort keyCode)
{
SendKey(keyCode, false);
}
public static void SimulateKeyUp(ushort keyCode)
{
SendKey(keyCode, true);
}
public static void SimulateKeyPress(ushort keyCode)
{
SendKey(keyCode, false);
SendKey(keyCode, true);
}
public static void SimulateShortcut(params ushort[] keyCodes)
{
foreach (var key in keyCodes)
{
SimulateKeyDown(key);
}
for (int i = keyCodes.Length - 1; i >= 0; i--)
{
SimulateKeyUp(keyCodes[i]);
}
}
public static void SendKey(ushort keyCode, bool keyUp)
{
var inputShift = new INPUT
{
type = INPUTTYPE.INPUT_KEYBOARD,
data = new InputUnion
{
ki = new KEYBDINPUT
{
wVk = (short)keyCode,
dwFlags = (uint)(keyUp ? KeyEventF.KeyUp : 0),
dwExtraInfo = (uint)IntPtr.Zero,
},
},
};
INPUT[] inputs = [inputShift];
_ = SendInput(1, inputs, INPUT.Size);
}
}
}

View File

@@ -1,74 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MouseUtils.UITests
{
public class MouseHighlighterSettings
{
// Appearance settings
public string Radius { get; set; }
public string FadeDelay { get; set; }
public string FadeDuration { get; set; }
// Color settings
public string PrimaryButtonHighlightColor { get; set; }
public string SecondaryButtonHighlightColor { get; set; }
public string AlwaysHighlightColor { get; set; }
// Settings UI Elements
public enum SettingsUIElements
{
PrimaryButtonHighlightColorGroup,
SecondaryButtonHighlightColorGroup,
AlwaysHighlightColorGroup,
RadiusEdit,
FadeDelayEdit,
FadeDurationEdit,
}
private Dictionary<SettingsUIElements, string> ElementNameMap { get; }
// Optional constructor to initialize properties
public MouseHighlighterSettings(
string radius = "",
string fadeDelay = "",
string fadeDuration = "",
string primaryButtonHighlightColor = "",
string secondaryButtonHighlightColor = "",
string alwaysHighlightColor = "")
{
this.Radius = radius;
this.FadeDelay = fadeDelay;
this.FadeDuration = fadeDuration;
this.PrimaryButtonHighlightColor = primaryButtonHighlightColor;
this.SecondaryButtonHighlightColor = secondaryButtonHighlightColor;
this.AlwaysHighlightColor = alwaysHighlightColor;
ElementNameMap = new Dictionary<SettingsUIElements, string>
{
[SettingsUIElements.PrimaryButtonHighlightColorGroup] = @"Primary button highlight color",
[SettingsUIElements.SecondaryButtonHighlightColorGroup] = @"Secondary button highlight color",
[SettingsUIElements.AlwaysHighlightColorGroup] = @"Always highlight color",
[SettingsUIElements.RadiusEdit] = @"Radius (px) Minimum5",
[SettingsUIElements.FadeDelayEdit] = @"Fade delay (ms) Minimum0",
[SettingsUIElements.FadeDurationEdit] = @"Fade duration (ms) Minimum0",
};
}
public string GetElementName(SettingsUIElements element)
{
return ElementNameMap[element];
}
}
}

View File

@@ -1,86 +0,0 @@
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Core.AnimationMetrics;
namespace MouseUtils.UITests
{
public class MousePointerCrosshairsSettings
{
// Appearance settings
public string Opacity { get; set; }
public string CenterRadius { get; set; }
public string Thickness { get; set; }
public string BorderSize { get; set; }
public bool IsFixLength { get; set; }
public string FixedLength { get; set; }
// Color settings
public string CrosshairsColor { get; set; }
public string CrosshairsBorderColor { get; set; }
// Settings UI Elements
public enum SettingsUIElements
{
CrosshairsColorGroup,
CrosshairsBorderColorGroup,
OpacitySlider,
CenterRadiusEdit,
ThicknessEdit,
BorderSizeEdit,
FixedLengthEdit,
IsFixLengthToggle,
}
private Dictionary<SettingsUIElements, string> ElementNameMap { get; }
// Optional constructor to initialize properties
public MousePointerCrosshairsSettings(
string opacity = "",
string centerRadius = "",
string thickness = "",
string borderSize = "",
bool isFixLength = false,
string fixedLength = "",
string crosshairsColor = "",
string crosshairsBorderColor = "")
{
this.Opacity = opacity;
this.CenterRadius = centerRadius;
this.Thickness = thickness;
this.BorderSize = borderSize;
this.IsFixLength = isFixLength;
this.FixedLength = fixedLength;
this.CrosshairsColor = crosshairsColor;
this.CrosshairsBorderColor = crosshairsBorderColor;
ElementNameMap = new Dictionary<SettingsUIElements, string>
{
[SettingsUIElements.CrosshairsColorGroup] = @"Crosshairs color",
[SettingsUIElements.CrosshairsBorderColorGroup] = @"Crosshairs border color",
[SettingsUIElements.OpacitySlider] = @"Crosshairs opacity (%)",
[SettingsUIElements.CenterRadiusEdit] = @"Crosshairs center radius (px) Minimum0 Maximum500",
[SettingsUIElements.ThicknessEdit] = @"Crosshairs thickness (px) Minimum1 Maximum50",
[SettingsUIElements.BorderSizeEdit] = @"Crosshairs border size (px) Minimum0 Maximum50",
[SettingsUIElements.FixedLengthEdit] = @"Crosshairs fixed length (px) Minimum1",
[SettingsUIElements.IsFixLengthToggle] = @"Fix crosshairs length",
};
}
public string GetElementName(SettingsUIElements element)
{
return ElementNameMap[element];
}
}
}

View File

@@ -1,59 +0,0 @@
// 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.Collections.Generic;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium.Windows;
namespace MouseUtils.UITests
{
public class MouseUtilsSettings
{
// Mouse Utils Modules
public enum MouseUtils
{
MouseHighlighter,
FindMyMouse,
MousePointerCrosshairs,
MouseJump,
}
private static readonly Dictionary<MouseUtils, string> MouseUtilUINameMap = new()
{
[MouseUtils.MouseHighlighter] = @"Mouse Highlighter",
[MouseUtils.FindMyMouse] = @"Find My Mouse",
[MouseUtils.MousePointerCrosshairs] = @"Mouse Pointer Crosshairs",
[MouseUtils.MouseJump] = @"Mouse Jump",
};
private static readonly Dictionary<MouseUtils, string> MouseUtilUIToggleMap = new()
{
[MouseUtils.MouseHighlighter] = @"Enable Mouse Highlighter",
[MouseUtils.FindMyMouse] = @"Enable Find My Mouse",
[MouseUtils.MousePointerCrosshairs] = @"Enable Mouse Pointer Crosshairs",
[MouseUtils.MouseJump] = @"Enable Mouse Jump",
};
public static string GetMouseUtilUIName(MouseUtils element)
{
return MouseUtilUINameMap[element];
}
public static void SetMouseUtilEnabled(Custom? custom, MouseUtils element, bool isEnable = true)
{
if (custom != null)
{
string toggleName = MouseUtilUIToggleMap[element];
var toggle = custom.Find<ToggleSwitch>(toggleName);
toggle.Toggle(isEnable);
}
else
{
Assert.Fail(element + " custom not found.");
}
}
}
}

View File

@@ -49,8 +49,6 @@ namespace WorkspacesEditor.Data
public WindowPositionWrapper Position { get; set; }
public int Monitor { get; set; }
public string Version { get; set; }
}
public struct MonitorConfigurationWrapper

View File

@@ -41,7 +41,6 @@ namespace WorkspacesEditor.Models
Maximized = other.Maximized;
Position = other.Position;
MonitorNumber = other.MonitorNumber;
Version = other.Version;
Parent = other.Parent;
IsNotFound = other.IsNotFound;
@@ -275,7 +274,5 @@ namespace WorkspacesEditor.Models
CommandLineArguments = newCommandLineValue;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppMainParams)));
}
public string Version { get; set; }
}
}

View File

@@ -246,7 +246,6 @@ namespace WorkspacesEditor.Models
AppPath = app.ApplicationPath,
AppTitle = app.Title,
PwaAppId = string.IsNullOrEmpty(app.PwaAppId) ? string.Empty : app.PwaAppId,
Version = string.IsNullOrEmpty(app.Version) ? string.Empty : app.Version,
PackageFullName = app.PackageFullName,
AppUserModelId = app.AppUserModelId,
Parent = this,

View File

@@ -107,7 +107,6 @@ namespace WorkspacesEditor.Utils
CommandLineArguments = app.CommandLineArguments,
IsElevated = app.IsElevated,
CanLaunchElevated = app.CanLaunchElevated,
Version = app.Version,
Maximized = app.Maximized,
Minimized = app.Minimized,
Position = new ProjectData.ApplicationWrapper.WindowPositionWrapper

View File

@@ -1,24 +0,0 @@
// 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.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WorkspacesEditorUITest;
[TestClass]
public class WorkspacesEditorTests : UITestBase
{
public WorkspacesEditorTests()
: base(PowerToysModule.Workspaces, WindowSize.Medium)
{
}
[TestMethod("WorkspacesEditor.Items.Present")]
[TestCategory("Workspaces UI")]
public void TestItemsPresents()
{
Assert.IsTrue(this.Has<Button>("Create Workspace"), "Should have create workspace button");
}
}

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<RunVSTest>false</RunVSTest>
<IsTestProject>true</IsTestProject>
<IsPackable>false</IsPackable>
<AssemblyName>PowerToys.Workspaces.UITests</AssemblyName>
</PropertyGroup>
<PropertyGroup>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\Workspaces.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Appium.WebDriver" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

View File

@@ -165,52 +165,16 @@ namespace AppLauncher
if (!launched && !app.pwaAppId.empty())
{
int version = 0;
if (app.version != L"")
std::filesystem::path appPath(app.path);
if (appPath.filename() == NonLocalizable::EdgeFilename)
{
try
{
version = std::stoi(app.version);
}
catch (const std::invalid_argument&)
{
Logger::error(L"Invalid version format: {}", app.version);
version = 0;
}
catch (const std::out_of_range&)
{
Logger::error(L"Version out of range: {}", app.version);
version = 0;
}
appPathFinal = appPath.parent_path() / NonLocalizable::EdgePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
if (version >= 1)
if (appPath.filename() == NonLocalizable::ChromeFilename)
{
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
if (res.isOk())
{
launched = true;
}
else
{
launchErrors.push_back({ app.appUserModelId, res.error() });
}
}
if (!launched)
{
std::filesystem::path appPath(app.path);
if (appPath.filename() == NonLocalizable::EdgeFilename)
{
appPathFinal = appPath.parent_path() / NonLocalizable::EdgePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
if (appPath.filename() == NonLocalizable::ChromeFilename)
{
appPathFinal = appPath.parent_path() / NonLocalizable::ChromePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
appPathFinal = appPath.parent_path() / NonLocalizable::ChromePwaFilename;
commandLineArgsFinal = NonLocalizable::PwaCommandLineAddition + app.pwaAppId + L" " + app.commandLineArgs;
}
}

View File

@@ -76,6 +76,19 @@ namespace WorkspacesLibUnitTests
Assert::IsTrue(result == nonExistentWindowAumid);
}
TEST_METHOD (PwaHelper_GetAUMIDFromWindow_InvalidWindow_ReturnsEmpty)
{
// Arrange
Utils::PwaHelper helper;
HWND invalidWindow = nullptr;
// Act
std::wstring result = helper.GetAUMIDFromWindow(invalidWindow);
// Assert
Assert::IsTrue(result.empty());
}
TEST_METHOD (PwaHelper_GetEdgeAppId_ValidConstruction_DoesNotCrash)
{
// Arrange

View File

@@ -1,6 +1,5 @@
#include "pch.h"
#include "PwaHelper.h"
#include "WindowUtils.h"
#include <filesystem>
@@ -52,7 +51,7 @@ namespace Utils
localFolder = L""; // Ensure it is explicitly set to empty on failure
}
}
return localFolder;
}
@@ -194,7 +193,7 @@ namespace Utils
return std::nullopt;
}
std::optional<std::wstring> PwaHelper::GetChromeAppId(const std::wstring& windowAumid) const
{
const auto appIdIndexStart = windowAumid.find(NonLocalizable::ChromeAppIdIdentifier);
@@ -257,4 +256,85 @@ namespace Utils
return result;
}
std::wstring PwaHelper::GetAUMIDFromWindow(HWND hwnd) const
{
std::wstring result{};
if (hwnd == NULL)
{
return result;
}
Microsoft::WRL::ComPtr<IPropertyStore> propertyStore;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&propertyStore));
if (FAILED(hr))
{
return result;
}
PROPVARIANT propVariant;
PropVariantInit(&propVariant);
hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &propVariant);
if (SUCCEEDED(hr) && propVariant.vt == VT_LPWSTR && propVariant.pwszVal != nullptr)
{
result = propVariant.pwszVal;
}
PropVariantClear(&propVariant);
Logger::info(L"Found a window with aumid {}", result);
return result;
}
std::wstring PwaHelper::GetAUMIDFromProcessId(DWORD processId) const
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
if (hProcess == NULL)
{
Logger::error(L"Failed to open process handle. Error: {}", get_last_error_or_default(GetLastError()));
return {};
}
// Get the package full name for the process
UINT32 packageFullNameLength = 0;
LONG rc = GetPackageFullName(hProcess, &packageFullNameLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get package full name length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> packageFullName(packageFullNameLength);
rc = GetPackageFullName(hProcess, &packageFullNameLength, packageFullName.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get package full name. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
// Get the AUMID for the package
UINT32 appModelIdLength = 0;
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get AppUserModelId length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> appModelId(appModelIdLength);
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, appModelId.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get AppUserModelId. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
CloseHandle(hProcess);
return std::wstring(appModelId.data());
}
}

View File

@@ -12,16 +12,19 @@ namespace Utils
PwaHelper();
~PwaHelper() = default;
std::optional<std::wstring> GetEdgeAppId(const std::wstring& windowAumid) const;
std::optional<std::wstring> GetChromeAppId(const std::wstring& windowAumid) const;
std::wstring SearchPwaName(const std::wstring& pwaAppId, const std::wstring& windowAumid) const;
std::wstring GetAUMIDFromWindow(HWND hWnd) const;
std::optional<std::wstring> GetEdgeAppId(const std::wstring& windowAumid) const;
std::optional<std::wstring> GetChromeAppId(const std::wstring& windowAumid) const;
std::wstring SearchPwaName(const std::wstring& pwaAppId, const std::wstring& windowAumid) const;
private:
void InitAppIds(const std::wstring& browserDataFolder, const std::wstring& browserDirPrefix, const std::function<void(const std::wstring&)>& addingAppIdCallback);
void InitEdgeAppIds();
void InitChromeAppIds();
std::wstring GetAppIdFromCommandLineArgs(const std::wstring& commandLineArgs) const;
std::wstring GetAUMIDFromProcessId(DWORD processId) const;
std::map<std::wstring, std::wstring> m_edgeAppIds;
std::vector<std::wstring> m_chromeAppIds;

View File

@@ -1,18 +0,0 @@
#include "pch.h"
#include "StringUtils.h"
namespace StringUtils
{
bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2)
{
if (str1.size() != str2.size())
{
return false;
}
return std::equal(str1.begin(), str1.end(), str2.begin(), [](wchar_t ch1, wchar_t ch2) {
return towupper(ch1) == towupper(ch2);
});
}
}

View File

@@ -6,5 +6,15 @@
namespace StringUtils
{
bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2);
inline bool CaseInsensitiveEquals(const std::wstring& str1, const std::wstring& str2)
{
if (str1.size() != str2.size())
{
return false;
}
return std::equal(str1.begin(), str1.end(), str2.begin(), [](wchar_t ch1, wchar_t ch2) {
return towupper(ch1) == towupper(ch2);
});
}
}

View File

@@ -1,105 +0,0 @@
#include "pch.h"
#include "WindowUtils.h"
#include <filesystem>
#include <appmodel.h>
#include <shellapi.h>
#include <ShlObj.h>
#include <shobjidl.h>
#include <tlhelp32.h>
#include <wrl.h>
#include <propkey.h>
#include <wil/com.h>
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/CommandLineArgsHelper.h>
#include <WorkspacesLib/StringUtils.h>
namespace Utils
{
std::wstring GetAUMIDFromProcessId(DWORD processId)
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
if (hProcess == NULL)
{
Logger::error(L"Failed to open process handle. Error: {}", get_last_error_or_default(GetLastError()));
return {};
}
// Get the package full name for the process
UINT32 packageFullNameLength = 0;
LONG rc = GetPackageFullName(hProcess, &packageFullNameLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get package full name length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> packageFullName(packageFullNameLength);
rc = GetPackageFullName(hProcess, &packageFullNameLength, packageFullName.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get package full name. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
// Get the AUMID for the package
UINT32 appModelIdLength = 0;
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, nullptr);
if (rc != ERROR_INSUFFICIENT_BUFFER)
{
Logger::error(L"Failed to get AppUserModelId length. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
std::vector<wchar_t> appModelId(appModelIdLength);
rc = GetApplicationUserModelId(hProcess, &appModelIdLength, appModelId.data());
if (rc != ERROR_SUCCESS)
{
Logger::error(L"Failed to get AppUserModelId. Error code: {}", rc);
CloseHandle(hProcess);
return {};
}
CloseHandle(hProcess);
return std::wstring(appModelId.data());
}
std::wstring GetAUMIDFromWindow(HWND hwnd)
{
std::wstring result{};
if (hwnd == NULL)
{
return result;
}
Microsoft::WRL::ComPtr<IPropertyStore> propertyStore;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&propertyStore));
if (FAILED(hr))
{
return result;
}
PROPVARIANT propVariant;
PropVariantInit(&propVariant);
hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &propVariant);
if (SUCCEEDED(hr) && propVariant.vt == VT_LPWSTR && propVariant.pwszVal != nullptr)
{
result = propVariant.pwszVal;
}
PropVariantClear(&propVariant);
Logger::info(L"Found a window with aumid {}", result);
return result;
}
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include <functional>
#include <WorkspacesLib/AppUtils.h>
#include <wtypes.h>
namespace Utils
{
std::wstring GetAUMIDFromWindow(HWND hWnd);
std::wstring GetAUMIDFromProcessId(DWORD processId);
};

View File

@@ -87,7 +87,6 @@ namespace WorkspacesData
const static wchar_t* MaximizedID = L"maximized";
const static wchar_t* PositionID = L"position";
const static wchar_t* MonitorID = L"monitor";
const static wchar_t* VersionID = L"version";
}
json::JsonObject ToJson(const WorkspacesProject::Application& data)
@@ -107,7 +106,6 @@ namespace WorkspacesData
json.SetNamedValue(NonLocalizable::MaximizedID, json::value(data.isMaximized));
json.SetNamedValue(NonLocalizable::PositionID, PositionJSON::ToJson(data.position));
json.SetNamedValue(NonLocalizable::MonitorID, json::value(data.monitor));
json.SetNamedValue(NonLocalizable::VersionID, json::value(data.version));
return json;
}
@@ -170,11 +168,6 @@ namespace WorkspacesData
result.position = position.value();
}
if (json.HasKey(NonLocalizable::VersionID))
{
result.version = json.GetNamedString(NonLocalizable::VersionID);
}
}
catch (const winrt::hresult_error&)
{
@@ -293,7 +286,6 @@ namespace WorkspacesData
const static wchar_t* MoveExistingWindowsID = L"move-existing-windows";
const static wchar_t* MonitorConfigurationID = L"monitor-configuration";
const static wchar_t* AppsID = L"applications";
const static wchar_t* Version = L"version";
}
json::JsonObject ToJson(const WorkspacesProject& data)

View File

@@ -33,9 +33,6 @@ namespace WorkspacesData
std::wstring appUserModelId;
std::wstring pwaAppId;
std::wstring commandLineArgs;
// empty to 1,
std::wstring version;
bool isElevated{};
bool canLaunchElevated{};
bool isMinimized{};
@@ -89,7 +86,7 @@ namespace WorkspacesData
{
WorkspacesData::WorkspacesProject::Application application;
HWND window{};
LaunchingState state{ LaunchingState::Waiting };
LaunchingState state { LaunchingState::Waiting };
};
using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;

View File

@@ -45,7 +45,6 @@
<ClInclude Include="StringUtils.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WbemHelper.h" />
<ClInclude Include="WindowUtils.h" />
<ClInclude Include="WorkspacesData.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
@@ -60,10 +59,8 @@
</ClCompile>
<ClCompile Include="PwaHelper.cpp" />
<ClCompile Include="SteamGameHelper.cpp" />
<ClCompile Include="StringUtils.cpp" />
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WbemHelper.cpp" />
<ClCompile Include="WindowUtils.cpp" />
<ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>

View File

@@ -56,9 +56,6 @@
<ClInclude Include="SteamHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -97,12 +94,6 @@
<ClCompile Include="SteamGameHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="StringUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -11,7 +11,6 @@
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/PwaHelper.h>
#include <WorkspacesLib/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include "Generated Files/resource.h"
@@ -140,7 +139,7 @@ namespace SnapshotUtils
bool isChrome = appData.IsChrome();
if (isEdge || isChrome)
{
auto windowAumid = Utils::GetAUMIDFromWindow(window);
auto windowAumid = pwaHelper.GetAUMIDFromWindow(window);
std::optional<std::wstring> pwaAppId{};
if (isEdge)
@@ -159,8 +158,6 @@ namespace SnapshotUtils
appData.pwaAppId = pwaAppId.value();
appData.name = pwaName + L" (" + appData.name + L")";
// If it's pwa app, appUserModelId should be their own pwa id.
appData.appUserModelId = windowAumid;
}
}
@@ -188,7 +185,6 @@ namespace SnapshotUtils
.appUserModelId = appData.appUserModelId,
.pwaAppId = appData.pwaAppId,
.commandLineArgs = L"",
.version = L"1",
.isElevated = IsProcessElevated(pid),
.canLaunchElevated = appData.canLaunchElevated,
.isMinimized = isMinimized,

View File

@@ -13,7 +13,6 @@
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
#include <WorkspacesLib/PwaHelper.h>
#include <WorkspacesLib/WindowUtils.h>
namespace NonLocalizable
{
@@ -204,7 +203,7 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
{
// Only care about steam games if it has no thick frame to remain consistent with
// Only care about steam games if it has no thick frame to remain consistent with
// the behavior as before.
continue;
}
@@ -221,7 +220,7 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
bool isChrome = appData.IsChrome();
if (isEdge || isChrome)
{
auto windowAumid = Utils::GetAUMIDFromWindow(window);
auto windowAumid = pwaHelper.GetAUMIDFromWindow(window);
std::optional<std::wstring> pwaAppId{};
if (isEdge)

View File

@@ -117,18 +117,16 @@ private:
for (DWORD pid : processIds)
{
HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, pid);
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL)
{
SetEvent(m_hTerminateEvent);
// Wait for 1.5 seconds for the process to end correctly, allowing time for ETW tracer and extensions to stop
if (WaitForSingleObject(hProcess, 1500) == WAIT_TIMEOUT)
{
TerminateProcess(hProcess, 0);
}
// Wait for 1.5 seconds for the process to end correctly and stop etw tracer
WaitForSingleObject(hProcess, 1500);
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
}
}

View File

@@ -3,6 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Windows.Data.Json;
@@ -22,14 +27,145 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
public string DataJson { get; protected set; } = "{}";
public AdaptiveCardParseResult? Card { get; private set; }
private static string Serialize(string? s) =>
JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
public override void InitializeProperties()
private static bool TryBuildCard(
string templateJson,
string dataJson,
out AdaptiveCardParseResult? card,
out Exception? error)
{
card = null;
error = null;
try
{
var template = new AdaptiveCardTemplate(templateJson);
var cardJson = template.Expand(dataJson);
card = AdaptiveCard.FromJsonString(cardJson);
return true;
}
catch (Exception ex)
{
Logger.LogError("Error building card from template: {Message}", ex.Message);
error = ex;
return false;
}
}
public void HandleSubmit()
public override void InitializeProperties()
{
var model = _formModel.Unsafe;
if (model is null)
{
return;
}
TemplateJson = model.TemplateJson;
StateJson = model.StateJson;
DataJson = model.DataJson;
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
{
Card = builtCard;
UpdateProperty(nameof(Card));
return;
}
var errorPayload = $$"""
{
"error_message": {{Serialize(renderingError!.Message)}},
"error_stack": {{Serialize(renderingError.StackTrace)}},
"inner_exception": {{Serialize(renderingError.InnerException?.Message)}},
"template_json": {{Serialize(TemplateJson)}},
"data_json": {{Serialize(DataJson)}}
}
""";
if (TryBuildCard(ErrorCardJson, errorPayload, out var errorCard, out var _))
{
Card = errorCard;
UpdateProperty(nameof(Card));
return;
}
UpdateProperty(nameof(Card));
}
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
{
if (action is AdaptiveOpenUrlAction openUrlAction)
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
return;
}
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
{
// Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{
try
{
var model = _formModel.Unsafe!;
if (model != null)
{
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
}
catch (Exception ex)
{
ShowException(ex);
}
});
}
}
private static readonly string ErrorCardJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Error parsing form from extension",
"wrap": true,
"style": "heading",
"size": "ExtraLarge",
"weight": "Bolder",
"color": "Attention"
},
{
"type": "TextBlock",
"wrap": true,
"text": "${error_message}",
"color": "Attention"
},
{
"type": "TextBlock",
"text": "${error_stack}",
"fontType": "Monospace"
},
{
"type": "TextBlock",
"wrap": true,
"text": "Inner exception:"
},
{
"type": "TextBlock",
"wrap": true,
"text": "${inner_exception}",
"color": "Attention"
}
]
}
""";
}

View File

@@ -17,6 +17,8 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="AdaptiveCards.Templating" />
<PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>

View File

@@ -107,17 +107,17 @@ public class ExtensionWrapper : IExtensionWrapper
{
Logger.LogDebug($"Starting {ExtensionDisplayName} ({ExtensionClassId})");
unsafe
var extensionPtr = nint.Zero;
try
{
var extensionPtr = (void*)nint.Zero;
try
{
// -2147024809: E_INVALIDARG
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
// -2147024809: E_INVALIDARG
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out extensionPtr);
unsafe
{
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
if (hr.Value == -2147024893)
{
@@ -128,15 +128,28 @@ public class ExtensionWrapper : IExtensionWrapper
return;
}
Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
}
finally
{
if ((nint)extensionPtr != nint.Zero)
extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
if (hr < 0)
{
Marshal.Release((nint)extensionPtr);
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
Marshal.ThrowExceptionForHR(hr);
}
// extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
extensionPtr = (nint)extensionObj;
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
}
}
finally
{
if (extensionPtr != nint.Zero)
{
Marshal.Release(extensionPtr);
}
}
}

View File

@@ -102,8 +102,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
HandleChangeAlias();
OnPropertyChanged(nameof(AliasText));
OnPropertyChanged(nameof(IsDirectAlias));
}
}
@@ -118,7 +116,6 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
HandleChangeAlias();
OnPropertyChanged(nameof(IsDirectAlias));
}
}

View File

@@ -67,7 +67,6 @@ public partial class App : Application
"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () =>
{
EtwTrace?.Dispose();
AppWindow?.Close();
Environment.Exit(0);
});
}

View File

@@ -0,0 +1,342 @@
// 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 AdaptiveCards.Rendering.WinUI3;
namespace Microsoft.CmdPal.UI.Controls;
public sealed class AdaptiveCardsConfig
{
public static AdaptiveHostConfig Light { get; }
public static AdaptiveHostConfig Dark { get; }
static AdaptiveCardsConfig()
{
Light = AdaptiveHostConfig.FromJsonString(LightHostConfigString).HostConfig;
Dark = AdaptiveHostConfig.FromJsonString(DarkHostConfigString).HostConfig;
}
public static readonly string DarkHostConfigString = """
{
"spacing": {
"small": 4,
"default": 8,
"medium": 20,
"large": 30,
"extraLarge": 40,
"padding": 8
},
"separator": {
"lineThickness": 0,
"lineColor": "#C8FFFFFF"
},
"supportsInteractivity": true,
"fontTypes": {
"default": {
"fontFamily": "'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
"fontSizes": {
"small": 12,
"default": 12,
"medium": 14,
"large": 20,
"extraLarge": 26
},
"fontWeights": {
"lighter": 200,
"default": 400,
"bolder": 600
}
},
"monospace": {
"fontFamily": "'Courier New', Courier, monospace",
"fontSizes": {
"small": 12,
"default": 12,
"medium": 14,
"large": 18,
"extraLarge": 26
},
"fontWeights": {
"lighter": 200,
"default": 400,
"bolder": 600
}
}
},
"containerStyles": {
"default": {
"backgroundColor": "#00000000",
"borderColor": "#00000000",
"foregroundColors": {
"default": {
"default": "#FFFFFF",
"subtle": "#C8FFFFFF"
},
"accent": {
"default": "#0063B1",
"subtle": "#880063B1"
},
"attention": {
"default": "#FF5555",
"subtle": "#DDFF5555"
},
"good": {
"default": "#54a254",
"subtle": "#DD54a254"
},
"warning": {
"default": "#c3ab23",
"subtle": "#DDc3ab23"
}
}
},
"emphasis": {
"backgroundColor": "#09FFFFFF",
"borderColor": "#09FFFFFF",
"foregroundColors": {
"default": {
"default": "#FFFFFF",
"subtle": "#C8FFFFFF"
},
"accent": {
"default": "#2E89FC",
"subtle": "#882E89FC"
},
"attention": {
"default": "#FF5555",
"subtle": "#DDFF5555"
},
"good": {
"default": "#54a254",
"subtle": "#DD54a254"
},
"warning": {
"default": "#c3ab23",
"subtle": "#DDc3ab23"
}
}
}
},
"imageSizes": {
"small": 16,
"medium": 24,
"large": 32
},
"actions": {
"maxActions": 5,
"spacing": "default",
"buttonSpacing": 8,
"showCard": {
"actionMode": "inline",
"inlineTopMargin": 8
},
"actionsOrientation": "horizontal",
"actionAlignment": "stretch"
},
"adaptiveCard": {
"allowCustomStyle": false
},
"imageSet": {
"imageSize": "medium",
"maxImageHeight": 100
},
"factSet": {
"title": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "bolder",
"wrap": true,
"maxWidth": 150
},
"value": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "default",
"wrap": true
},
"spacing": 8
},
"textStyles": {
"heading": {
"size": "large",
"weight": "bolder",
"color": "default",
"isSubtle": false,
"fontType": "default"
},
"columnHeader": {
"size": "medium",
"weight": "bolder",
"color": "default",
"isSubtle": false,
"fontType": "default"
}
}
}
""";
public static readonly string LightHostConfigString = """
{
"spacing": {
"small": 4,
"default": 8,
"medium": 20,
"large": 30,
"extraLarge": 40,
"padding": 8
},
"separator": {
"lineThickness": 0,
"lineColor": "#606060"
},
"supportsInteractivity": true,
"fontTypes": {
"default": {
"fontFamily": "'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
"fontSizes": {
"small": 12,
"default": 12,
"medium": 14,
"large": 20,
"extraLarge": 26
},
"fontWeights": {
"lighter": 200,
"default": 400,
"bolder": 600
}
},
"monospace": {
"fontFamily": "'Courier New', Courier, monospace",
"fontSizes": {
"small": 12,
"default": 12,
"medium": 14,
"large": 18,
"extraLarge": 26
},
"fontWeights": {
"lighter": 200,
"default": 400,
"bolder": 600
}
}
},
"containerStyles": {
"default": {
"backgroundColor": "#00000000",
"borderColor": "#00000000",
"foregroundColors": {
"default": {
"default": "#E6000000",
"subtle": "#99000000"
},
"accent": {
"default": "#0063B1",
"subtle": "#880063B1"
},
"attention": {
"default": "#C00000",
"subtle": "#DDC00000"
},
"good": {
"default": "#54a254",
"subtle": "#DD54a254"
},
"warning": {
"default": "#c3ab23",
"subtle": "#DDc3ab23"
}
}
},
"emphasis": {
"backgroundColor": "#80F6F6F6",
"borderColor": "#80F6F6F6",
"foregroundColors": {
"default": {
"default": "#E6000000",
"subtle": "#99000000"
},
"accent": {
"default": "#2E89FC",
"subtle": "#882E89FC"
},
"attention": {
"default": "#C00000",
"subtle": "#DDC00000"
},
"good": {
"default": "#54a254",
"subtle": "#DD54a254"
},
"warning": {
"default": "#c3ab23",
"subtle": "#DDc3ab23"
}
}
}
},
"imageSizes": {
"small": 16,
"medium": 24,
"large": 32
},
"actions": {
"maxActions": 5,
"spacing": "default",
"buttonSpacing": 8,
"showCard": {
"actionMode": "inline",
"inlineTopMargin": 8
},
"actionsOrientation": "horizontal",
"actionAlignment": "stretch"
},
"adaptiveCard": {
"allowCustomStyle": false
},
"imageSet": {
"imageSize": "medium",
"maxImageHeight": 100
},
"factSet": {
"title": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "bolder",
"wrap": true,
"maxWidth": 150
},
"value": {
"color": "default",
"size": "default",
"isSubtle": false,
"weight": "default",
"wrap": true
},
"spacing": 8
},
"textStyles": {
"heading": {
"size": "large",
"weight": "bolder",
"color": "default",
"isSubtle": false,
"fontType": "default"
},
"columnHeader": {
"size": "medium",
"weight": "bolder",
"color": "default",
"isSubtle": false,
"fontType": "default"
}
}
}
""";
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.ContentFormControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
Background="Transparent"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary x:Name="CardOverrideStyles">
<Style x:Key="Adaptive.TextBlock" TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid x:Name="ContentGrid" />
</UserControl>

View File

@@ -1,32 +1,162 @@
// 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 AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Rendering.WinUI3;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class ContentFormControl : UserControl
{
#pragma warning disable CS0649
private static readonly AdaptiveCardRenderer _renderer;
private ContentFormViewModel? _viewModel;
#pragma warning restore CS0649
// LOAD-BEARING: if you don't hang onto a reference to the RenderedAdaptiveCard
// then the GC might clean it up sometime, even while the card is in the UI
// tree. If this gets GC'ed, then it'll revoke our Action handler, and the
// form will do seemingly nothing.
private RenderedAdaptiveCard? _renderedCard;
public ContentFormViewModel? ViewModel { get => _viewModel; set => AttachViewModel(value); }
static ContentFormControl()
{
// We can't use `CardOverrideStyles` here yet, because we haven't called InitializeComponent once.
// But also, the default value isn't `null` here. It's... some other default empty value.
// So clear it out so that we know when the first time we get created is
_renderer = new AdaptiveCardRenderer()
{
OverrideStyles = null,
};
}
public ContentFormControl()
{
this.InitializeComponent();
var lightTheme = ActualTheme == Microsoft.UI.Xaml.ElementTheme.Light;
_renderer.HostConfig = lightTheme ? AdaptiveCardsConfig.Light : AdaptiveCardsConfig.Dark;
// 5% BODGY: if we set this multiple times over the lifetime of the app,
// then the second call will explode, because "CardOverrideStyles is already the child of another element".
// SO only set this once.
if (_renderer.OverrideStyles == null)
{
_renderer.OverrideStyles = CardOverrideStyles;
}
// TODO in the future, we should handle ActualThemeChanged and replace
// our rendered card with one for that theme. But today is not that day
}
private void AttachViewModel(ContentFormViewModel? vm)
{
if (_viewModel != null)
{
_viewModel.PropertyChanged -= ViewModel_PropertyChanged;
}
_viewModel = vm;
if (_viewModel != null)
{
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
var c = _viewModel.Card;
if (c != null)
{
DisplayCard(c);
}
}
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (ViewModel == null)
{
return;
}
if (e.PropertyName == nameof(ViewModel.Card))
{
var c = ViewModel.Card;
if (c != null)
{
DisplayCard(c);
}
}
}
private void DisplayCard(AdaptiveCardParseResult result)
{
_renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard);
ContentGrid.Children.Clear();
if (_renderedCard.FrameworkElement != null)
{
ContentGrid.Children.Add(_renderedCard.FrameworkElement);
// Use the Loaded event to ensure we focus after the card is in the visual tree
_renderedCard.FrameworkElement.Loaded += OnFrameworkElementLoaded;
}
_renderedCard.Action += Rendered_Action;
}
private void OnFrameworkElementLoaded(object sender, RoutedEventArgs e)
{
// Unhook the event handler to avoid multiple registrations
if (sender is FrameworkElement element)
{
element.Loaded -= OnFrameworkElementLoaded;
if (!ViewModel?.OnlyControlOnPage ?? true)
{
return;
}
// Focus on the first focusable element asynchronously to ensure the visual tree is fully built
element.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
var focusableElement = FindFirstFocusableElement(element);
focusableElement?.Focus(FocusState.Programmatic);
});
}
}
private Control? FindFirstFocusableElement(DependencyObject parent)
{
var childCount = VisualTreeHelper.GetChildrenCount(parent);
// Process children first (depth-first search)
for (var i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
// If the child is a focusable control like TextBox, ComboBox, etc.
if (child is Control control &&
control.IsEnabled &&
control.IsTabStop &&
control.Visibility == Visibility.Visible &&
control.AllowFocusOnInteraction)
{
return control;
}
// Recursively check children
var result = FindFirstFocusableElement(child);
if (result != null)
{
return result;
}
}
return null;
}
private void Rendered_Action(RenderedAdaptiveCard sender, AdaptiveActionEventArgs args) =>
ViewModel?.HandleSubmit(args.Action, args.Inputs.AsJson());
}

View File

@@ -12,7 +12,7 @@ public delegate bool IsActive();
public delegate bool FilterAccessibleKeyboardEvents(int key, UIntPtr extraInfo);
public partial class HotkeySettingsControlHook : IDisposable
public class HotkeySettingsControlHook : IDisposable
{
private const int WmKeyDown = 0x100;
private const int WmKeyUp = 0x101;

View File

@@ -5,81 +5,82 @@
using System;
using System.Runtime.InteropServices;
namespace Microsoft.PowerToys.Settings.UI.Helpers;
internal static class NativeKeyboardHelper
namespace Microsoft.PowerToys.Settings.UI.Helpers
{
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct INPUT
internal static class NativeKeyboardHelper
{
internal INPUTTYPE type;
internal InputUnion data;
internal static int Size
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct INPUT
{
get { return Marshal.SizeOf<INPUT>(); }
internal INPUTTYPE type;
internal InputUnion data;
internal static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
}
}
[StructLayout(LayoutKind.Explicit)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal uint dwFlags;
internal uint time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct KEYBDINPUT
{
internal short wVk;
internal short wScan;
internal uint dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct HARDWAREINPUT
{
internal int uMsg;
internal short wParamL;
internal short wParamH;
}
internal enum INPUTTYPE : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2,
}
[Flags]
internal enum KeyEventF
{
KeyDown = 0x0000,
ExtendedKey = 0x0001,
KeyUp = 0x0002,
Unicode = 0x0004,
Scancode = 0x0008,
}
}
[StructLayout(LayoutKind.Explicit)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal uint dwFlags;
internal uint time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct KEYBDINPUT
{
internal short wVk;
internal short wScan;
internal uint dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
internal struct HARDWAREINPUT
{
internal int uMsg;
internal short wParamL;
internal short wParamH;
}
internal enum INPUTTYPE : uint
{
INPUT_MOUSE = 0,
INPUT_KEYBOARD = 1,
INPUT_HARDWARE = 2,
}
[Flags]
internal enum KeyEventF
{
KeyDown = 0x0000,
ExtendedKey = 0x0001,
KeyUp = 0x0002,
Unicode = 0x0004,
Scancode = 0x0008,
}
}

Some files were not shown because too many files have changed in this diff Show More