Compare commits
181 Commits
leilzh/fuz
...
yaqingmi/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13995652ce | ||
|
|
1c7c777a32 | ||
|
|
f7dae375d7 | ||
|
|
6d9fccdbee | ||
|
|
e1316e6631 | ||
|
|
4d1dbea5d1 | ||
|
|
46aa43cb69 | ||
|
|
01359c4b0c | ||
|
|
508b95cf5b | ||
|
|
5946b98f87 | ||
|
|
e61e5b6d83 | ||
|
|
67272ad073 | ||
|
|
a79126373f | ||
|
|
3688995511 | ||
|
|
255a11df85 | ||
|
|
d713a3001d | ||
|
|
614af1dd77 | ||
|
|
469ffd93ea | ||
|
|
797941954d | ||
|
|
8a07b7b560 | ||
|
|
f0a23ceaeb | ||
|
|
f2373cf259 | ||
|
|
cfdcf91625 | ||
|
|
2f678d1fb3 | ||
|
|
f49625210c | ||
|
|
602eef8830 | ||
|
|
90a489352b | ||
|
|
13a6287dea | ||
|
|
6cb852077a | ||
|
|
cdc5f073f0 | ||
|
|
0427a7a7b0 | ||
|
|
2d0d12f06c | ||
|
|
06e5db6ff0 | ||
|
|
1a097ae09c | ||
|
|
980ca46cdb | ||
|
|
6a1999d601 | ||
|
|
5655c61794 | ||
|
|
371b7f0868 | ||
|
|
b6bcc92eb4 | ||
|
|
2ac464279a | ||
|
|
8ce198a47b | ||
|
|
15ef9189ba | ||
|
|
2c555e2c2b | ||
|
|
83817700e1 | ||
|
|
7e92a9a5e9 | ||
|
|
fe067def65 | ||
|
|
6b9c99c2f6 | ||
|
|
ba6af794ac | ||
|
|
9cb99be4e9 | ||
|
|
c2dddab4ac | ||
|
|
1eb60302df | ||
|
|
11781d11e9 | ||
|
|
ad974bd679 | ||
|
|
49e5bbb5f0 | ||
|
|
7efbd2f013 | ||
|
|
ba230eca07 | ||
|
|
30df5e0df2 | ||
|
|
9a6c64f9c0 | ||
|
|
7dc2a05c45 | ||
|
|
1a14dce5a3 | ||
|
|
26fe36ab8d | ||
|
|
06b56a10bd | ||
|
|
fc804a8156 | ||
|
|
f63fcfd91c | ||
|
|
195ff24a85 | ||
|
|
5691c5754b | ||
|
|
100d560f9e | ||
|
|
25c29ade8e | ||
|
|
8dfa55fe28 | ||
|
|
6c317c4ee1 | ||
|
|
54e058e82d | ||
|
|
b0e7473760 | ||
|
|
f085ba0cd2 | ||
|
|
884bfc71d3 | ||
|
|
252cf2670f | ||
|
|
211bb15be0 | ||
|
|
2752b217e5 | ||
|
|
4be6129835 | ||
|
|
583614449d | ||
|
|
b0afc57e77 | ||
|
|
d4e577bb81 | ||
|
|
e1ad7e39c6 | ||
|
|
e8b02cd797 | ||
|
|
2eb51a1861 | ||
|
|
91077c71a9 | ||
|
|
997622fac8 | ||
|
|
232e1b79bd | ||
|
|
5ec2728dea | ||
|
|
d314fa075e | ||
|
|
90723d5b12 | ||
|
|
ddb9d8ecc6 | ||
|
|
f2a5505601 | ||
|
|
311ab88ec3 | ||
|
|
856cbaa293 | ||
|
|
4067be17b2 | ||
|
|
b4020c6720 | ||
|
|
f87c1bc448 | ||
|
|
c04400e7df | ||
|
|
05218e8af6 | ||
|
|
f49daa24e0 | ||
|
|
c1c9720992 | ||
|
|
750bb429c5 | ||
|
|
7f5aaf26dc | ||
|
|
af8d5402c8 | ||
|
|
6cf73ce839 | ||
|
|
77852f2137 | ||
|
|
b902d3adf0 | ||
|
|
82ff0615d4 | ||
|
|
5081d6d31e | ||
|
|
abec5eb96a | ||
|
|
71ec39ff89 | ||
|
|
017b9e6339 | ||
|
|
c8b1a925b3 | ||
|
|
e0e3772cdd | ||
|
|
6b8798fb94 | ||
|
|
836cfbf698 | ||
|
|
91d504511f | ||
|
|
3d3682672c | ||
|
|
b9474e9f60 | ||
|
|
c5b29188b8 | ||
|
|
e59b6d3051 | ||
|
|
6625289a10 | ||
|
|
2f0dae347f | ||
|
|
f3ddba8aa5 | ||
|
|
4744d23857 | ||
|
|
c42f8b0a1b | ||
|
|
70da33783d | ||
|
|
5fa416b962 | ||
|
|
aa7d89c8de | ||
|
|
fd7cccefa2 | ||
|
|
ae2bb61d26 | ||
|
|
52b4150e3b | ||
|
|
6dc0211867 | ||
|
|
fc4d3a96c3 | ||
|
|
a347d740ca | ||
|
|
e8359931ac | ||
|
|
1cb215d1a6 | ||
|
|
0f6ff59d2e | ||
|
|
25a8c95049 | ||
|
|
c60c79af67 | ||
|
|
dd5279b9b8 | ||
|
|
78921eb7d3 | ||
|
|
9173ac27df | ||
|
|
ce57d818a7 | ||
|
|
595231f64b | ||
|
|
7894c1e5ad | ||
|
|
d2d71bf797 | ||
|
|
3eb5ca294a | ||
|
|
0277938f18 | ||
|
|
dd7b6f3ea9 | ||
|
|
0187d1abcd | ||
|
|
7bd2b50126 | ||
|
|
b127611462 | ||
|
|
622770134e | ||
|
|
ceca607142 | ||
|
|
77a5ef7d32 | ||
|
|
75f2b0927c | ||
|
|
f54ab6ebd5 | ||
|
|
dd5997ab06 | ||
|
|
6bdbb6b552 | ||
|
|
84dd551d84 | ||
|
|
83bb82322d | ||
|
|
c4f8f09fab | ||
|
|
9c95835384 | ||
|
|
1867ac8f02 | ||
|
|
127e079efe | ||
|
|
03540e307c | ||
|
|
a10578f7d3 | ||
|
|
9c08c957e5 | ||
|
|
c73b88f575 | ||
|
|
fc1c4abd0a | ||
|
|
22efeb3f63 | ||
|
|
63c4089441 | ||
|
|
fd206ecdee | ||
|
|
d243b58715 | ||
|
|
abf4626843 | ||
|
|
3874a3b893 | ||
|
|
8687b310db | ||
|
|
c7ed8ee0c3 | ||
|
|
8c21f794af | ||
|
|
18befd7149 |
3
.github/actions/spell-check/allow/code.txt
vendored
@@ -271,6 +271,7 @@ mengyuanchen
|
||||
|
||||
# DllName
|
||||
testhost
|
||||
Testably
|
||||
|
||||
#Tools
|
||||
OIP
|
||||
OIP
|
||||
28
.github/actions/spell-check/expect.txt
vendored
@@ -8,6 +8,7 @@ Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -197,6 +198,7 @@ CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
closesocket
|
||||
clp
|
||||
CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
@@ -273,6 +275,7 @@ CURSORINFO
|
||||
cursorpos
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
CVal
|
||||
cvd
|
||||
CVirtual
|
||||
@@ -312,6 +315,7 @@ debugbreak
|
||||
declatory
|
||||
decryptor
|
||||
Dedup
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
DEFAULTFLAGS
|
||||
@@ -323,6 +327,7 @@ DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
DEFT
|
||||
deinitialization
|
||||
DELA
|
||||
DELETEDKEYIMAGE
|
||||
@@ -343,6 +348,7 @@ devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
devpal
|
||||
DFX
|
||||
DIALOGEX
|
||||
digicert
|
||||
dimm
|
||||
@@ -517,11 +523,13 @@ FRAMECHANGED
|
||||
frm
|
||||
Froml
|
||||
FROMTOUCH
|
||||
fsanitize
|
||||
fsmgmt
|
||||
FZE
|
||||
gacutil
|
||||
Gaeilge
|
||||
Gaidhlig
|
||||
gameid
|
||||
GC'ed
|
||||
GCLP
|
||||
gdi
|
||||
@@ -628,6 +636,7 @@ HOTKEYF
|
||||
hotkeys
|
||||
hotlight
|
||||
hotspot
|
||||
Hostx
|
||||
HPAINTBUFFER
|
||||
HRAWINPUT
|
||||
HREDRAW
|
||||
@@ -711,6 +720,7 @@ INPUTSINK
|
||||
INPUTTYPE
|
||||
INSTALLDESKTOPSHORTCUT
|
||||
INSTALLDIR
|
||||
installdir
|
||||
INSTALLFOLDER
|
||||
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
|
||||
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
@@ -1005,6 +1015,7 @@ netsh
|
||||
newcolor
|
||||
NEWDIALOGSTYLE
|
||||
NEWFILE
|
||||
NEWFILEHEADER
|
||||
newitem
|
||||
newpath
|
||||
newplus
|
||||
@@ -1038,6 +1049,7 @@ NOINHERITLAYOUT
|
||||
NOINTERFACE
|
||||
NOINVERT
|
||||
NOLINKINFO
|
||||
nologo
|
||||
NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
@@ -1075,6 +1087,7 @@ NOTRACK
|
||||
NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
NOTXORPEN
|
||||
notwindows
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
@@ -1269,6 +1282,7 @@ pstm
|
||||
PStr
|
||||
pstream
|
||||
pstrm
|
||||
pswd
|
||||
PSYSTEM
|
||||
psz
|
||||
ptb
|
||||
@@ -1299,6 +1313,7 @@ QUNS
|
||||
QXZ
|
||||
RAII
|
||||
RAlt
|
||||
Rappl
|
||||
randi
|
||||
Rasterization
|
||||
Rasterize
|
||||
@@ -1395,6 +1410,7 @@ sacl
|
||||
safeprojectname
|
||||
SAMEKEYPREVIOUSLYMAPPED
|
||||
SAMESHORTCUTPREVIOUSLYMAPPED
|
||||
sancov
|
||||
SAVEFAILED
|
||||
scanled
|
||||
schedtasks
|
||||
@@ -1413,6 +1429,7 @@ searchterm
|
||||
SEARCHUI
|
||||
SECONDARYDISPLAY
|
||||
secpol
|
||||
securestring
|
||||
SEEMASKINVOKEIDLIST
|
||||
SELCHANGE
|
||||
SENDCHANGE
|
||||
@@ -1566,6 +1583,7 @@ stdcpp
|
||||
stdcpplatest
|
||||
STDMETHODCALLTYPE
|
||||
STDMETHODIMP
|
||||
steamapps
|
||||
STGC
|
||||
STGM
|
||||
STGMEDIUM
|
||||
@@ -1647,6 +1665,7 @@ telephon
|
||||
templatenamespace
|
||||
testprocess
|
||||
TEXCOORD
|
||||
TEXTBOXNEWLINE
|
||||
TEXTEXTRACTOR
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
@@ -1928,6 +1947,7 @@ WUX
|
||||
Wwanpp
|
||||
XAxis
|
||||
xclip
|
||||
xcopy
|
||||
XDocument
|
||||
XElement
|
||||
xfd
|
||||
@@ -1966,3 +1986,11 @@ ZOOMITX
|
||||
ZXk
|
||||
ZXNs
|
||||
zzz
|
||||
ACIE
|
||||
AOklab
|
||||
BCIE
|
||||
BOklab
|
||||
culori
|
||||
Evercoder
|
||||
LCh
|
||||
CIELCh
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
"WinUI3Apps\\PowerToys.Settings.exe",
|
||||
|
||||
"PowerToys.CmdPalModuleInterface.dll",
|
||||
"CmdPalKeyboardService.dll",
|
||||
"*Microsoft.CmdPal.UI_*.msix"
|
||||
],
|
||||
"SigningInfo": {
|
||||
@@ -330,6 +331,8 @@
|
||||
"TestableIO.System.IO.Abstractions.Wrappers.dll",
|
||||
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
|
||||
"WinUI3Apps\\OpenAI.dll",
|
||||
"Testably.Abstractions.FileSystem.Interface.dll",
|
||||
"WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
|
||||
"ColorCode.Core.dll",
|
||||
"ColorCode.UWP.dll",
|
||||
"UnitsNet.dll",
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
Param(
|
||||
# Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
[Parameter(Mandatory=$False,Position=1)]
|
||||
[string]$winAppSdkVersionNumber = "1.6",
|
||||
[string]$winAppSdkVersionNumber = "1.7",
|
||||
|
||||
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
|
||||
[Parameter(Mandatory=$False,Position=2)]
|
||||
[boolean]$useExperimentalVersion = $False
|
||||
[boolean]$useExperimentalVersion = $False,
|
||||
|
||||
# Root folder Path for processing
|
||||
[Parameter(Mandatory=$False,Position=3)]
|
||||
[string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
|
||||
|
||||
# Root folder Path for processing
|
||||
[Parameter(Mandatory=$False,Position=4)]
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = "nuget.config"
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
@@ -35,7 +43,33 @@ function Update-NugetConfig {
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
|
||||
$content = $reader.ReadToEnd()
|
||||
$encoding = $reader.CurrentEncoding
|
||||
$reader.Close()
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Content = $content
|
||||
Encoding = $encoding
|
||||
}
|
||||
}
|
||||
|
||||
function Write-FileWithEncoding {
|
||||
param (
|
||||
[string]$Path,
|
||||
[string]$Content,
|
||||
[System.Text.Encoding]$Encoding
|
||||
)
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
|
||||
$writer.Write($Content)
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
@@ -79,50 +113,54 @@ if ($latestVersion) {
|
||||
}
|
||||
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Recurse packages.config | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
$propsFile = "Directory.Packages.props"
|
||||
$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
|
||||
if (Test-Path $propsFile) {
|
||||
$content = Get-Content $propsFile -Raw
|
||||
$file = Read-FileWithEncoding -Path $propsFile
|
||||
$content = $file.Content
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $propsFile -Value $content
|
||||
Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $propsFile
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Recurse *.csproj | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.PowerToys.Telemetry" version="2.0.2" />
|
||||
<package id="Microsoft.PowerToys.Telemetry" version="2.0.3" />
|
||||
</packages>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"notificationAliases": ["powertoys@microsoft.com"],
|
||||
"instanceUrl": "https://microsoft.visualstudio.com",
|
||||
"projectName": "OS",
|
||||
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
|
||||
"areaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\PowerToys"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.6
|
||||
default: 1.7
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -112,10 +112,12 @@ extends:
|
||||
# Prepare the localizations and telemetry config before the release build
|
||||
- template: .pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml@self
|
||||
|
||||
- script: |
|
||||
call nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config || exit /b 1
|
||||
move /Y "Microsoft.PowerToys.Telemetry.2.0.2\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h" || exit /b 1
|
||||
move /Y "Microsoft.PowerToys.Telemetry.2.0.2\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs" || exit /b 1
|
||||
- pwsh: |-
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
& nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config
|
||||
Move-Item -Force -Verbose "Microsoft.PowerToys.Telemetry.*\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h"
|
||||
Move-Item -Force -Verbose "Microsoft.PowerToys.Telemetry.*\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs"
|
||||
displayName: Emplace telemetry files
|
||||
|
||||
- stage: Build_SDK
|
||||
@@ -148,5 +150,7 @@ extends:
|
||||
parameters:
|
||||
versionNumber: ${{ parameters.versionNumber }}
|
||||
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
|
||||
${{ if ne(parameters.publishSymbolsToPublic, true) }}:
|
||||
symbolExpiryTime: 10 # For private builds, expire symbols within 10 days. The default is 100 years.
|
||||
subscription: $(SymbolPublishingServiceConnection)
|
||||
symbolProject: $(SymbolPublishingProject)
|
||||
|
||||
@@ -50,6 +50,9 @@ parameters:
|
||||
- name: runTests
|
||||
type: boolean
|
||||
default: true
|
||||
- name: buildTests
|
||||
type: boolean
|
||||
default: true
|
||||
- name: useVSPreview
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -110,7 +113,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 eq(parameters.runTests, true) }}:
|
||||
${{ if or(eq(parameters.runTests, true), eq(parameters.buildTests, true)) }}:
|
||||
MSBuildMainBuildTargets: Build;Test
|
||||
${{ else }}:
|
||||
MSBuildMainBuildTargets: Build
|
||||
@@ -344,6 +347,11 @@ jobs:
|
||||
flattenFolders: True
|
||||
OverWrite: True
|
||||
|
||||
# Check if all projects (located in src sub-folder) import common props
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyCommonProps.ps1' -sourceDir '$(build.sourcesdirectory)\src'
|
||||
displayName: Audit shared common props for CSharp projects in src sub-folder
|
||||
|
||||
# Check if deps.json files don't reference different dll versions.
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)'
|
||||
@@ -514,7 +522,7 @@ jobs:
|
||||
displayName: Stage GPO files
|
||||
|
||||
# Running the tests may result in future jobs consuming artifacts out of this build
|
||||
- ${{ if eq(parameters.runTests, true) }}:
|
||||
- ${{ if or(eq(parameters.runTests, true), eq(parameters.buildTests, true)) }}:
|
||||
- task: CopyFiles@2
|
||||
displayName: Stage entire build output
|
||||
inputs:
|
||||
|
||||
@@ -11,24 +11,36 @@ parameters:
|
||||
- name: useLatestWebView2
|
||||
type: boolean
|
||||
default: false
|
||||
- name: isUIAutomationPipeline
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
- job: Test${{ parameters.platform }}${{ parameters.configuration }}
|
||||
displayName: Test ${{ parameters.platform }} ${{ parameters.configuration }}
|
||||
timeoutInMinutes: 300
|
||||
variables:
|
||||
BuildPlatform: ${{ parameters.platform }}
|
||||
${{ if or(eq(parameters.platform, 'x64Win10'), eq(parameters.platform, 'x64Win11')) }}:
|
||||
BuildPlatform: x64
|
||||
${{ else }}:
|
||||
BuildPlatform: ${{ parameters.platform }}
|
||||
TestPlatform: ${{ parameters.platform }}
|
||||
BuildConfiguration: ${{ parameters.configuration }}
|
||||
SrcPath: $(Build.Repository.LocalPath)
|
||||
TestArtifactsName: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}
|
||||
TestArtifactsName: build-${{ variables.BuildPlatform }}-${{ 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 and(eq(parameters.isUIAutomationPipeline, true), 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 and(eq(parameters.isUIAutomationPipeline, true), eq(parameters.platform, 'x64Win11')) }}:
|
||||
demands: ImageOverride -equals SHINE-W11-Testing
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-Testing-arm64
|
||||
steps:
|
||||
@@ -101,8 +113,16 @@ jobs:
|
||||
vsTestVersion: 'toolsInstaller'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
testAssemblyVer2: |
|
||||
**\UITests-FancyZones.dll
|
||||
**\UITests-FancyZonesEditor.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
${{ if eq(parameters.isUIAutomationPipeline, true) }}:
|
||||
testAssemblyVer2: |
|
||||
**\*UITest*.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
${{ else }}:
|
||||
testAssemblyVer2: |
|
||||
**\UITests-FancyZones.dll
|
||||
**\UITests-FancyZonesEditor.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
env:
|
||||
platform: '$(TestPlatform)'
|
||||
|
||||
87
.pipelines/v2/templates/pipeline-ui-tests-automation.yml
Normal file
@@ -0,0 +1,87 @@
|
||||
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 }}
|
||||
isUIAutomationPipeline: true
|
||||
|
||||
- ${{ 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 }}
|
||||
isUIAutomationPipeline: true
|
||||
|
||||
- ${{ 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 }}
|
||||
isUIAutomationPipeline: true
|
||||
@@ -17,6 +17,7 @@ steps:
|
||||
arguments: >
|
||||
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
|
||||
54
.pipelines/verifyCommonProps.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$sourceDir
|
||||
)
|
||||
|
||||
# scan all csharp project in the source directory
|
||||
function Get-CSharpProjects {
|
||||
param (
|
||||
[string]$path
|
||||
)
|
||||
|
||||
# Get all .csproj files under the specified path
|
||||
return Get-ChildItem -Path $path -Recurse -Filter *.csproj | Select-Object -ExpandProperty FullName
|
||||
}
|
||||
|
||||
# Check if the project file imports 'Common.Dotnet.CsWinRT.props'
|
||||
function Test-ImportSharedCsWinRTProps {
|
||||
param (
|
||||
[string]$filePath
|
||||
)
|
||||
|
||||
# Load the XML content of the .csproj file
|
||||
[xml]$csprojContent = Get-Content -Path $filePath
|
||||
|
||||
|
||||
# Check if the Import element with Project attribute containing 'Common.Dotnet.CsWinRT.props' exists
|
||||
return $csprojContent.Project.Import | Where-Object { $null -ne $_.Project -and $_.Project.EndsWith('Common.Dotnet.CsWinRT.props') }
|
||||
}
|
||||
|
||||
# Call the function with the provided source directory
|
||||
$csprojFilesArray = Get-CSharpProjects -path $sourceDir
|
||||
|
||||
$hasInvalidCsProj = $false
|
||||
|
||||
# Enumerate the array of file paths and call Validate-ImportSharedCsWinRTProps for each file
|
||||
foreach ($csprojFile in $csprojFilesArray) {
|
||||
# Skip if the file ends with 'TemplateCmdPalExtension.csproj'
|
||||
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
|
||||
continue
|
||||
}
|
||||
|
||||
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
|
||||
if (!$importExists) {
|
||||
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."
|
||||
$hasInvalidCsProj = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasInvalidCsProj) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -91,5 +91,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
|
||||
|
||||
exit 0
|
||||
@@ -31,22 +31,22 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.5" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.120-preview" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
|
||||
<!--
|
||||
@@ -55,7 +55,7 @@
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
@@ -72,28 +72,28 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.4" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.5" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.4" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.5" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.5" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.5" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
|
||||
80
NOTICE.md
@@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE.
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
```
|
||||
|
||||
## Utility: Command Palette Built-in Extensions
|
||||
|
||||
### Calculator
|
||||
|
||||
#### Mages
|
||||
|
||||
We use the Mages NuGet package for calculating the result of expression.
|
||||
|
||||
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
|
||||
|
||||
```
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2025 Florian Rappl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: File Explorer Add-ins
|
||||
|
||||
### Monaco Editor
|
||||
@@ -1419,27 +1453,27 @@ SOFTWARE.
|
||||
- Mages 3.0.0
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 3.1.3
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.4
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.5
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.4
|
||||
- Microsoft.Data.Sqlite 9.0.5
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.4
|
||||
- Microsoft.Extensions.Hosting 9.0.4
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.4
|
||||
- Microsoft.Extensions.Logging 9.0.4
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.4
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.5
|
||||
- Microsoft.Extensions.Hosting 9.0.5
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.5
|
||||
- Microsoft.Extensions.Logging 9.0.5
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.5
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2903.40
|
||||
- Microsoft.Win32.SystemEvents 9.0.4
|
||||
- Microsoft.Windows.Compatibility 9.0.4
|
||||
- Microsoft.Win32.SystemEvents 9.0.5
|
||||
- Microsoft.Windows.Compatibility 9.0.5
|
||||
- Microsoft.Windows.CsWin32 0.2.46-beta
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
|
||||
- Microsoft.WindowsAppSDK 1.6.250205002
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
|
||||
- Microsoft.WindowsAppSDK 1.7.250401001
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
|
||||
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
|
||||
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
|
||||
- ModernWpfUI 0.9.4
|
||||
@@ -1453,25 +1487,25 @@ SOFTWARE.
|
||||
- SharpCompress 0.37.2
|
||||
- StreamJsonRpc 2.21.69
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 9.0.4
|
||||
- System.CodeDom 9.0.5
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.4
|
||||
- System.Configuration.ConfigurationManager 9.0.4
|
||||
- System.Data.OleDb 9.0.4
|
||||
- System.ComponentModel.Composition 9.0.5
|
||||
- System.Configuration.ConfigurationManager 9.0.5
|
||||
- System.Data.OleDb 9.0.5
|
||||
- System.Data.SqlClient 4.8.6
|
||||
- System.Diagnostics.EventLog 9.0.4
|
||||
- System.Diagnostics.PerformanceCounter 9.0.4
|
||||
- System.Drawing.Common 9.0.4
|
||||
- System.Diagnostics.EventLog 9.0.5
|
||||
- System.Diagnostics.PerformanceCounter 9.0.5
|
||||
- System.Drawing.Common 9.0.5
|
||||
- System.IO.Abstractions 22.0.13
|
||||
- System.IO.Abstractions.TestingHelpers 22.0.13
|
||||
- System.Management 9.0.4
|
||||
- System.Management 9.0.5
|
||||
- System.Net.Http 4.3.4
|
||||
- System.Private.Uri 4.3.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.4
|
||||
- System.ServiceProcess.ServiceController 9.0.4
|
||||
- System.Text.Encoding.CodePages 9.0.4
|
||||
- System.Text.Json 9.0.4
|
||||
- System.Runtime.Caching 9.0.5
|
||||
- System.ServiceProcess.ServiceController 9.0.5
|
||||
- System.Text.Encoding.CodePages 9.0.5
|
||||
- System.Text.Json 9.0.5
|
||||
- System.Text.RegularExpressions 4.3.1
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
|
||||
@@ -708,6 +708,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2580,6 +2586,22 @@ 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
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -2588,6 +2610,12 @@ Global
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -2727,7 +2755,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} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
{7AC943C9-52E8-44CF-9083-744D8049667B} = {322566EF-20DC-43A6-B9F8-616AF942579A}
|
||||
{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}
|
||||
@@ -2858,7 +2886,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}
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
117
doc/Fuzzing/CppFuzzingGuide.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 🧪 C++ Project Fuzzing Test Guide
|
||||
|
||||
This guide walks you through setting up a **fuzzing test** project for a C++ module using [libFuzzer](https://llvm.org/docs/LibFuzzer.html).
|
||||
.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Step-by-Step Setup
|
||||
|
||||
### 1. Create a New C++ Project
|
||||
|
||||
- Use **Empty Project** template.
|
||||
- Name it `<ModuleName>.FuzzingTest`.
|
||||
|
||||
---
|
||||
|
||||
### 2. Update Build Configuration
|
||||
|
||||
- In **Configuration Manager**, Uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations.
|
||||
- Note: ARM64 is not supported in this case, so leave ARM64 configurations build disabled.
|
||||
---
|
||||
|
||||
### 3. Enable ASan and libFuzzer in `.vcxproj`
|
||||
|
||||
Edit the project file to enable fuzzing:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<EnableASAN>true</EnableASAN>
|
||||
<EnableFuzzer>true</EnableFuzzer>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Add Fuzzing Compiler Flags
|
||||
|
||||
Add this to `AdditionalOptions` under the `Fuzzing` configuration:
|
||||
|
||||
```xml
|
||||
/fsanitize=address
|
||||
/fsanitize-coverage=inline-8bit-counters
|
||||
/fsanitize-coverage=edge
|
||||
/fsanitize-coverage=trace-cmp
|
||||
/fsanitize-coverage=trace-div
|
||||
%(AdditionalOptions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Link the Sanitizer Coverage Runtime
|
||||
|
||||
In `Linker → Input → Additional Dependencies`, add:
|
||||
|
||||
```text
|
||||
$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Copy Required Runtime DLL
|
||||
|
||||
Add a `PostBuildEvent` to copy the ASAN DLL:
|
||||
|
||||
```xml
|
||||
<Command>
|
||||
xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
|
||||
</Command>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Add Preprocessor Definitions
|
||||
|
||||
To avoid annotation issues, add these to the `Preprocessor Definitions`:
|
||||
|
||||
```text
|
||||
_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧬 Required Code
|
||||
|
||||
### `LLVMFuzzerTestOneInput` Entry Point
|
||||
|
||||
Every fuzzing project must expose this function:
|
||||
|
||||
```cpp
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
|
||||
{
|
||||
std::string input(reinterpret_cast<const char*>(data), size);
|
||||
|
||||
try
|
||||
{
|
||||
// Call your module with the input here.
|
||||
}
|
||||
catch (...) {}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ [Test run in the cloud](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/faq/notwindows/walkthrough)
|
||||
|
||||
To submit a job to the cloud you can run with this command:
|
||||
|
||||
```
|
||||
oip submit --config .\OneFuzzConfig.json --drop-path <your_submission_directory> --platform windows --do-not-file-bugs --duration 1
|
||||
```
|
||||
You want to run with --do-not-file-bugs because if there is an issue with running the parser in the cloud (which is very possible), you don't want bugs to be created if there is an issue. The --duration task is the number of hours you want the task to run. I recommend just running for 1 hour to make sure things work initially. If you don't specify this parameter, it will default to 48 hours. You can find more about submitting a test job here.
|
||||
|
||||
OneFuzz will send you an email when the job has started.
|
||||
|
||||
---
|
||||
@@ -21,67 +21,74 @@
|
||||
|
||||
- Create a new project and add the following references to the project file. Change the OutputPath to your own module's path.
|
||||
```
|
||||
<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>
|
||||
<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>
|
||||
|
||||
```
|
||||
- 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 RunFancyZonesTest : UITestBase
|
||||
{
|
||||
public RunFancyZonesTest()
|
||||
: base(PowerToysModule.FancyZone)
|
||||
{
|
||||
}
|
||||
}
|
||||
[TestClass]
|
||||
public class HostModuleTests : UITestBase
|
||||
{
|
||||
public HostModuleTests()
|
||||
: base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Then you can start using session to perform the UI operations.
|
||||
- Then you can start performing the UI operations.
|
||||
|
||||
**Example**
|
||||
```
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace UITests_KeyboardManager
|
||||
[TestMethod("Hosts.Basic.EmptyViewShouldWork")]
|
||||
[TestCategory("Hosts File Editor #4")]
|
||||
public void TestEmptyView()
|
||||
{
|
||||
[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");
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
|
||||
// Maximize window
|
||||
var window = Session.Find<Window>(By.Name("Remap keys")).Maximize();
|
||||
// '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");
|
||||
|
||||
// Add Key Remapping
|
||||
this.Session.Find<Button>(By.Name("Add key remapping")).Click();
|
||||
window.Close();
|
||||
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"), "EmptyView");
|
||||
|
||||
// Back to Settings
|
||||
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -79,10 +79,7 @@
|
||||
<ComponentGroupRef Id="ToolComponentGroup" />
|
||||
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
|
||||
<ComponentGroupRef Id="WorkspacesComponentGroup" />
|
||||
|
||||
<?if $(var.CIBuild) = "true" ?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
<?endif?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
</Feature>
|
||||
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Some items may be set in Directory.Build.props in root -->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
|
||||
10
src/Common.Dotnet.FuzzTest.props
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Some items may be set in Directory.Build.props in root -->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- OneFuzz does not currently support testing with .NET 9.
|
||||
As a temporary workaround, create a .NET 8 project and use file links
|
||||
to include the code that needs testing. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
16
src/Common.Dotnet.PrepareGeneratedFolder.targets
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<Target Name="EnsureGeneratedBaseFolder" BeforeTargets="XamlPreCompile">
|
||||
<PropertyGroup>
|
||||
<!-- Only create the base 'generated' folder -->
|
||||
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Create 'generated' folder if missing -->
|
||||
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
|
||||
|
||||
<!-- Optional logging for debugging -->
|
||||
<Message Text="Ensured: $(GeneratedBasePath)" Importance="Low" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -141,6 +141,40 @@ namespace ManagedCommon
|
||||
return lab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklab color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
|
||||
{
|
||||
var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
|
||||
var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
|
||||
return oklab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklch color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
|
||||
{
|
||||
var oklab = ConvertToOklabColor(color);
|
||||
var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
|
||||
|
||||
return oklch;
|
||||
}
|
||||
|
||||
public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
|
||||
{
|
||||
// inverse companding, gamma correction must be undone
|
||||
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
|
||||
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
|
||||
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
|
||||
return (rLinear, gLinear, bLinear);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
|
||||
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
|
||||
@@ -156,10 +190,7 @@ namespace ManagedCommon
|
||||
double g = color.G / 255d;
|
||||
double b = color.B / 255d;
|
||||
|
||||
// inverse companding, gamma correction must be undone
|
||||
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
|
||||
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
|
||||
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
|
||||
(double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
|
||||
|
||||
return (
|
||||
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
|
||||
@@ -210,6 +241,63 @@ namespace ManagedCommon
|
||||
return (l, a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a linear RGB color <see cref="double"/> to an Oklab color.
|
||||
/// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
|
||||
/// and the implementation is based on https://bottosson.github.io/posts/oklab/
|
||||
/// </summary>
|
||||
/// <param name="r">Linear R value</param>
|
||||
/// <param name="g">Linear G value</param>
|
||||
/// <param name="b">Linear B value</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
private static (double Lightness, double ChromaticityA, double ChromaticityB)
|
||||
GetOklabColorFromLinearRGB(double r, double g, double b)
|
||||
{
|
||||
double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
|
||||
double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
|
||||
double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
|
||||
|
||||
double l_ = Math.Cbrt(l);
|
||||
double m_ = Math.Cbrt(m);
|
||||
double s_ = Math.Cbrt(s);
|
||||
|
||||
return (
|
||||
(0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
|
||||
(1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
|
||||
(0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
|
||||
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
private static (double Lightness, double Chroma, double Hue)
|
||||
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
|
||||
{
|
||||
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The lightness, chroma, and hue angle</returns>
|
||||
private static (double Lightness, double Chroma, double Hue)
|
||||
GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
|
||||
{
|
||||
// Lab to LCh transformation
|
||||
double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
|
||||
double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
|
||||
return (lightness, chroma, hue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
|
||||
/// </summary>
|
||||
@@ -276,12 +364,17 @@ namespace ManagedCommon
|
||||
{ "Br", 'p' }, // brightness percent
|
||||
{ "In", 'p' }, // intensity percent
|
||||
{ "Ll", 'p' }, // lightness (HSL) percent
|
||||
{ "Lc", 'p' }, // lightness(CIELAB)percent
|
||||
{ "Va", 'p' }, // value percent
|
||||
{ "Wh", 'p' }, // whiteness percent
|
||||
{ "Bn", 'p' }, // blackness percent
|
||||
{ "Ca", 'p' }, // chromaticityA percent
|
||||
{ "Cb", 'p' }, // chromaticityB percent
|
||||
{ "Lc", 'p' }, // lightness (CIE) percent
|
||||
{ "Ca", 'p' }, // chromaticityA (CIELAB) percent
|
||||
{ "Cb", 'p' }, // chromaticityB (CIELAB) percent
|
||||
{ "Lo", 'p' }, // lightness (Oklab/Oklch) percent
|
||||
{ "Oa", 'p' }, // chromaticityA (Oklab) percent
|
||||
{ "Ob", 'p' }, // chromaticityB (Oklab) percent
|
||||
{ "Oc", 'p' }, // chroma (Oklch) percent
|
||||
{ "Oh", 'p' }, // hue angle (Oklch) percent
|
||||
{ "Xv", 'i' }, // X value int
|
||||
{ "Yv", 'i' }, // Y value int
|
||||
{ "Zv", 'i' }, // Z value int
|
||||
@@ -424,6 +517,10 @@ namespace ManagedCommon
|
||||
var (lightnessC, _, _) = ConvertToCIELABColor(color);
|
||||
lightnessC = Math.Round(lightnessC, 2);
|
||||
return lightnessC.ToString(CultureInfo.InvariantCulture);
|
||||
case "Lo":
|
||||
var (lightnessO, _, _) = ConvertToOklabColor(color);
|
||||
lightnessO = Math.Round(lightnessO, 2);
|
||||
return lightnessO.ToString(CultureInfo.InvariantCulture);
|
||||
case "Wh":
|
||||
var (_, whiteness, _) = ConvertToHWBColor(color);
|
||||
whiteness = Math.Round(whiteness * 100);
|
||||
@@ -440,6 +537,22 @@ namespace ManagedCommon
|
||||
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
|
||||
chromaticityB = Math.Round(chromaticityB, 2);
|
||||
return chromaticityB.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oa":
|
||||
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
|
||||
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
|
||||
return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
|
||||
case "Ob":
|
||||
var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
|
||||
chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
|
||||
return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oc":
|
||||
var (_, chromaOklch, _) = ConvertToOklchColor(color);
|
||||
chromaOklch = Math.Round(chromaOklch, 2);
|
||||
return chromaOklch.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oh":
|
||||
var (_, _, hueOklch) = ConvertToOklchColor(color);
|
||||
hueOklch = Math.Round(hueOklch, 2);
|
||||
return hueOklch.ToString(CultureInfo.InvariantCulture);
|
||||
case "Xv":
|
||||
var (x, _, _) = ConvertToCIEXYZColor(color);
|
||||
x = Math.Round(x * 100, 4);
|
||||
@@ -495,8 +608,10 @@ namespace ManagedCommon
|
||||
case "HSI": return "hsi(%Hu, %Si%, %In%)";
|
||||
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
|
||||
case "NCol": return "%Hn, %Wh%, %Bn%";
|
||||
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
|
||||
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
|
||||
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
|
||||
case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
|
||||
case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
|
||||
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
|
||||
case "Decimal": return "%Dv";
|
||||
case "HEX Int": return "0xFF%ReX%GrX%BlX";
|
||||
|
||||
51
src/common/ManagedCommon/IdRecoveryHelper.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public static class IdRecoveryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixes invalid IDs in the given list by assigning unique values.
|
||||
/// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items that may contain invalid IDs.</param>
|
||||
public static void RecoverInvalidIds<T>(IEnumerable<T> items)
|
||||
where T : class, IHasId
|
||||
{
|
||||
var idSet = new HashSet<int>();
|
||||
int newId = 0;
|
||||
var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
|
||||
|
||||
// Iterate through the list and fix invalid IDs
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
// If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
|
||||
if (!idSet.Add(item.Id))
|
||||
{
|
||||
// Find the next available unique ID
|
||||
while (idSet.Contains(newId))
|
||||
{
|
||||
newId++;
|
||||
}
|
||||
|
||||
item.Id = newId;
|
||||
idSet.Add(newId); // Add the newly assigned ID to the set
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHasId
|
||||
{
|
||||
int Id { get; set; }
|
||||
}
|
||||
}
|
||||
1095
src/common/UITestAutomation/Doc/ui-automation-cover-list.md
Normal file
28
src/common/UITestAutomation/Element/CheckBox.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/common/UITestAutomation/Element/ComboBox.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/common/UITestAutomation/Element/Custom.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -25,8 +26,20 @@ 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()
|
||||
@@ -112,9 +125,10 @@ 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>
|
||||
public virtual void Click(bool rightClick = false)
|
||||
public virtual void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
PerformAction(
|
||||
(actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement);
|
||||
|
||||
@@ -131,7 +145,9 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
});
|
||||
},
|
||||
msPreAction,
|
||||
msPostAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,51 +168,20 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drag element move offset.
|
||||
/// Release action
|
||||
/// </summary>
|
||||
/// <param name="offsetX">The offsetX to move.</param>
|
||||
/// <param name="offsetY">The offsetY to move.</param>
|
||||
public void Drag(int offsetX, int offsetY)
|
||||
public void ReleaseAction()
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
|
||||
actions.Build().Perform();
|
||||
});
|
||||
var releaseAction = new Actions(driver);
|
||||
releaseAction.Release().Perform();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drag element move to other element.
|
||||
/// Release key
|
||||
/// </summary>
|
||||
/// <param name="element">Move to this element.</param>
|
||||
public void Drag(Element element)
|
||||
public void ReleaseKey(Key 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);
|
||||
});
|
||||
KeyboardHelper.ReleaseKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,7 +203,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 = 3000)
|
||||
public T Find<T>(By by, int timeoutMS = 5000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
@@ -226,7 +211,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
// leverage findAll to filter out mismatched elements
|
||||
var collection = this.FindAll<T>(by, timeoutMS);
|
||||
|
||||
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
|
||||
Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
|
||||
|
||||
return collection[0];
|
||||
}
|
||||
@@ -239,7 +224,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 = 3000)
|
||||
public T Find<T>(string name, int timeoutMS = 5000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Find<T>(By.Name(name), timeoutMS);
|
||||
@@ -252,7 +237,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 = 3000)
|
||||
public Element Find(By by, int timeoutMS = 5000)
|
||||
{
|
||||
return this.Find<Element>(by, timeoutMS);
|
||||
}
|
||||
@@ -264,7 +249,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 = 3000)
|
||||
public Element Find(string name, int timeoutMS = 5000)
|
||||
{
|
||||
return this.Find<Element>(By.Name(name), timeoutMS);
|
||||
}
|
||||
@@ -276,7 +261,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 = 3000)
|
||||
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
@@ -308,7 +293,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 = 3000)
|
||||
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.FindAll<T>(By.Name(name), timeoutMS);
|
||||
@@ -321,7 +306,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 = 3000)
|
||||
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000)
|
||||
{
|
||||
return this.FindAll<Element>(by, timeoutMS);
|
||||
}
|
||||
@@ -333,11 +318,23 @@ 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 = 3000)
|
||||
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000)
|
||||
{
|
||||
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>
|
||||
@@ -360,5 +357,15 @@ 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 SaveToFile with parameter: path = {path}");
|
||||
this.windowsElement.GetScreenshot().SaveAsFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/common/UITestAutomation/Element/Group.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,12 @@ 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>
|
||||
public override void Click(bool rightClick = false)
|
||||
/// <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)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
PerformAction(
|
||||
(actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement, 10, 10);
|
||||
|
||||
@@ -40,7 +43,36 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
});
|
||||
},
|
||||
msPreAction,
|
||||
msPostAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 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>
|
||||
|
||||
67
src/common/UITestAutomation/Element/Pane.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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>
|
||||
/// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/common/UITestAutomation/Element/Slider.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/common/UITestAutomation/Element/Tab.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
// 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>
|
||||
@@ -35,9 +33,10 @@ namespace Microsoft.PowerToys.UITest
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
// select all text and delete it
|
||||
windowElement.SendKeys(Keys.Control + "a");
|
||||
windowElement.SendKeys(Keys.Delete);
|
||||
windowElement.SendKeys(OpenQA.Selenium.Keys.Control + "a");
|
||||
windowElement.SendKeys(OpenQA.Selenium.Keys.Delete);
|
||||
});
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
PerformAction((actions, windowElement) =>
|
||||
|
||||
36
src/common/UITestAutomation/Element/Thumb.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
var items = FindElementsWithRetry(findElementsFunc, timeoutMS);
|
||||
var res = items.Select(item =>
|
||||
{
|
||||
var element = item as WindowsElement;
|
||||
@@ -43,6 +43,27 @@ namespace Microsoft.PowerToys.UITest
|
||||
return new ReadOnlyCollection<T>(res);
|
||||
}
|
||||
|
||||
private static ReadOnlyCollection<TW> FindElementsWithRetry<TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, int timeoutMS)
|
||||
{
|
||||
int retryIntervalMS = 500;
|
||||
timeoutMS = 1;
|
||||
int elapsedTime = 0;
|
||||
|
||||
while (elapsedTime < timeoutMS)
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
if (items.Count > 0)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
Task.Delay(retryIntervalMS).Wait();
|
||||
elapsedTime += retryIntervalMS;
|
||||
}
|
||||
|
||||
return new ReadOnlyCollection<TW>(new List<TW>());
|
||||
}
|
||||
|
||||
public static T NewElement<T>(WindowsElement? element, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
{
|
||||
@@ -50,11 +71,6 @@ 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);
|
||||
|
||||
475
src/common/UITestAutomation/KeyboardHelper.cs
Normal file
@@ -0,0 +1,475 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,49 @@ 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
|
||||
@@ -52,6 +95,8 @@ 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.
|
||||
@@ -60,6 +105,8 @@ 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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
37
src/common/UITestAutomation/MonitorInfoData.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
217
src/common/UITestAutomation/MouseHelper.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/common/UITestAutomation/ScreenCapture.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Linq;
|
||||
using System.Text;
|
||||
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
|
||||
{
|
||||
@@ -17,35 +19,70 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
public class Session
|
||||
{
|
||||
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||
public WindowsDriver<WindowsElement> Root { get; set; }
|
||||
|
||||
private WindowsDriver<WindowsElement> WindowsDriver { get; set; }
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(nint hWnd);
|
||||
private List<IntPtr> windowHandlers = new List<IntPtr>();
|
||||
|
||||
public Session(WindowsDriver<WindowsElement> root, WindowsDriver<WindowsElement> windowsDriver)
|
||||
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> root, WindowsDriver<WindowsElement> windowsDriver, PowerToysModule scope, WindowSize size)
|
||||
{
|
||||
this.MainWindowHandler = IntPtr.Zero;
|
||||
this.Root = root;
|
||||
this.WindowsDriver = windowsDriver;
|
||||
this.InitScope = scope;
|
||||
|
||||
if (size != WindowSize.UnSpecified)
|
||||
{
|
||||
// Attach to the scope & reset MainWindowHandler
|
||||
this.Attach(scope, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an element by selector.
|
||||
/// Cleans up the Session Exe.
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
windowHandlers.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an 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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(By by, int timeoutMS = 3000)
|
||||
public T Find<T>(By by, int timeoutMS = 5000, bool global = false)
|
||||
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);
|
||||
var collection = this.FindAll<T>(by, timeoutMS, global);
|
||||
|
||||
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
|
||||
Assert.IsTrue(collection.Count > 0, $"UI-Element({typeof(T).Name}) not found using selector: {by}");
|
||||
|
||||
return collection[0];
|
||||
}
|
||||
@@ -55,62 +92,159 @@ 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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public T Find<T>(string name, int timeoutMS = 3000)
|
||||
public T Find<T>(string name, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Find<T>(By.Name(name), timeoutMS);
|
||||
return this.Find<T>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(By by, int timeoutMS = 3000)
|
||||
public Element Find(By by, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Find<Element>(by, timeoutMS);
|
||||
return this.Find<Element>(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
public Element Find(string name, int timeoutMS = 3000)
|
||||
public Element Find(string name, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Find<Element>(By.Name(name), timeoutMS);
|
||||
return this.Find<Element>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// 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.
|
||||
/// </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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
|
||||
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
|
||||
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}");
|
||||
var foundElements = FindHelper.FindAll<T, WindowsElement>(
|
||||
() =>
|
||||
{
|
||||
if (by.GetIsAccessibilityId())
|
||||
{
|
||||
var elements = this.WindowsDriver.FindElementsByAccessibilityId(by.GetAccessibilityId());
|
||||
var elements = driver.FindElementsByAccessibilityId(by.GetAccessibilityId());
|
||||
return elements;
|
||||
}
|
||||
else
|
||||
{
|
||||
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
|
||||
var elements = driver.FindElements(by.ToSeleniumBy());
|
||||
return elements;
|
||||
}
|
||||
},
|
||||
this.WindowsDriver,
|
||||
driver,
|
||||
timeoutMS);
|
||||
|
||||
return foundElements ?? new ReadOnlyCollection<T>([]);
|
||||
@@ -122,12 +256,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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
|
||||
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.FindAll<T>(By.Name(name), timeoutMS);
|
||||
return this.FindAll<T>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,11 +269,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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
|
||||
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.FindAll<Element>(by, timeoutMS);
|
||||
return this.FindAll<Element>(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -147,55 +281,187 @@ 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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
|
||||
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.FindAll<Element>(By.Name(name), timeoutMS);
|
||||
return this.FindAll<Element>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyboard Action key.
|
||||
/// Close the main window.
|
||||
/// </summary>
|
||||
/// <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 = "")
|
||||
public void CloseMainWindow()
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
if (MainWindow != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key2))
|
||||
{
|
||||
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);
|
||||
}
|
||||
MainWindow.Close();
|
||||
MainWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
actions.Release();
|
||||
actions.Build().Perform();
|
||||
/// <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(
|
||||
() =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <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));
|
||||
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)
|
||||
public Session Attach(PowerToysModule module, WindowSize size = WindowSize.UnSpecified)
|
||||
{
|
||||
string windowName = ModuleConfigData.Instance.GetModuleWindowName(module);
|
||||
return this.Attach(windowName);
|
||||
return this.Attach(windowName, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,26 +469,44 @@ 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)
|
||||
public Session Attach(string windowName, WindowSize size = WindowSize.UnSpecified)
|
||||
{
|
||||
this.IsElevated = null;
|
||||
this.MainWindowHandler = IntPtr.Zero;
|
||||
|
||||
if (this.Root != null)
|
||||
{
|
||||
var window = this.Root.FindElementByName(windowName);
|
||||
Assert.IsNotNull(window, $"Failed to attach. Window '{windowName}' not found");
|
||||
// search window handler by window title (admin and non-admin titles)
|
||||
var matchingWindows = WindowHelper.ApiHelper.FindDesktopWindowHandler([windowName, WindowHelper.AdministratorPrefix + windowName]);
|
||||
if (matchingWindows.Count == 0 || matchingWindows[0].HWnd == IntPtr.Zero)
|
||||
{
|
||||
Assert.Fail($"Failed to attach. Window '{windowName}' not found");
|
||||
}
|
||||
|
||||
// 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 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");
|
||||
|
||||
// Set implicit timeout to make element search retry every 500 ms
|
||||
this.WindowsDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(3);
|
||||
this.windowHandlers.Add(this.MainWindowHandler);
|
||||
|
||||
if (size != WindowSize.UnSpecified)
|
||||
{
|
||||
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
|
||||
}
|
||||
|
||||
// Set MainWindow
|
||||
MainWindow = Find<Window>(matchingWindows[0].Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -232,6 +516,61 @@ namespace Microsoft.PowerToys.UITest
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -254,5 +593,26 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
// 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 WindowsDriver<WindowsElement> Root { get; set; }
|
||||
@@ -27,10 +29,14 @@ namespace Microsoft.PowerToys.UITest
|
||||
private WindowsDriver<WindowsElement>? Driver { get; set; }
|
||||
|
||||
private Process? appDriver;
|
||||
private Process? runner;
|
||||
|
||||
private PowerToysModule scope;
|
||||
|
||||
[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);
|
||||
|
||||
@@ -40,14 +46,24 @@ namespace Microsoft.PowerToys.UITest
|
||||
Verb = "runas",
|
||||
};
|
||||
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
this.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = locationPath + this.runnerPath,
|
||||
Verb = "runas",
|
||||
};
|
||||
|
||||
if (scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
this.ExitExe(runnerProcessInfo.FileName);
|
||||
this.runner = Process.Start(runnerProcessInfo);
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -56,7 +72,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <param name="scope">The PowerToys module to start.</param>
|
||||
public SessionHelper Init()
|
||||
{
|
||||
this.StartExe(locationPath + this.sessionPath);
|
||||
// Exit setting exe to fix CommandPalette error, remove after fixing the issue
|
||||
this.ExitExe(this.locationPath + ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings));
|
||||
|
||||
this.ExitExe(this.locationPath + this.sessionPath);
|
||||
this.StartExe(this.locationPath + this.sessionPath);
|
||||
|
||||
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
|
||||
|
||||
@@ -73,6 +93,11 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
appDriver?.Kill();
|
||||
appDriver?.WaitForExit(); // Optional: Wait for the process to exit
|
||||
if (this.scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
runner?.Kill();
|
||||
runner?.WaitForExit(); // Optional: Wait for the process to exit
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -81,31 +106,15 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new exe and takes control of it.
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
public void StartExe(string appPath)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
opts.AddAdditionalCapability("app", appPath);
|
||||
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>
|
||||
/// Exit a exe.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the application executable.</param>
|
||||
public void ExitExe(string path)
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
public void ExitExe(string appPath)
|
||||
{
|
||||
// Exit Exe
|
||||
string exeName = Path.GetFileNameWithoutExtension(path);
|
||||
string exeName = Path.GetFileNameWithoutExtension(appPath);
|
||||
|
||||
// PowerToys.FancyZonesEditor
|
||||
Process[] processes = Process.GetProcessesByName(exeName);
|
||||
foreach (Process process in processes)
|
||||
{
|
||||
@@ -121,6 +130,39 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new exe and takes control of it.
|
||||
/// </summary>
|
||||
/// <param name="appPath">The path to the application executable.</param>
|
||||
public void StartExe(string appPath)
|
||||
{
|
||||
var opts = new AppiumOptions();
|
||||
opts.AddAdditionalCapability("app", appPath);
|
||||
|
||||
// Create driver with retry
|
||||
var timeout = TimeSpan.FromMinutes(2);
|
||||
var retryInterval = TimeSpan.FromSeconds(5);
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (DateTime.Now - startTime > timeout)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
Task.Delay(retryInterval).Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exit now exe.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<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>
|
||||
|
||||
@@ -6,10 +6,16 @@ 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
|
||||
{
|
||||
@@ -17,19 +23,38 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// Base class that should be inherited by all Test Classes.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class UITestBase
|
||||
public class UITestBase : IDisposable
|
||||
{
|
||||
public Session Session { get; set; }
|
||||
public required TestContext TestContext { get; set; }
|
||||
|
||||
private readonly SessionHelper sessionHelper;
|
||||
public required Session Session { get; set; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
private readonly bool isInPipeline;
|
||||
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)
|
||||
// private System.Threading.Timer? screenshotTimer;
|
||||
// private string? screenshotDirectory;
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified)
|
||||
{
|
||||
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.sessionHelper = new SessionHelper(scope).Init();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +63,21 @@ namespace Microsoft.PowerToys.UITest
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
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
|
||||
@@ -50,12 +90,32 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UnInitializes the test.
|
||||
/// Cleanups the test.
|
||||
/// </summary>
|
||||
[TestCleanup]
|
||||
public void TestClean()
|
||||
public void TestCleanup()
|
||||
{
|
||||
this.sessionHelper.Cleanup();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,47 +124,143 @@ 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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T Find<T>(By by, int timeoutMS = 3000)
|
||||
protected T Find<T>(By by, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.Find<T>(by, timeoutMS);
|
||||
return this.Session.Find<T>(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// Shortcut for this.Session.Find<Element>(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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected T Find<T>(string name, int timeoutMS = 3000)
|
||||
protected T Find<T>(string name, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.Find<T>(By.Name(name), timeoutMS);
|
||||
return this.Session.Find<T>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element Find(By by, int timeoutMS = 3000)
|
||||
protected Element Find(By by, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Session.Find(by, timeoutMS);
|
||||
return this.Session.Find(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
|
||||
/// Shortcut for this.Session.Find<Element>(name, timeoutMS)
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the element.</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>The found element.</returns>
|
||||
protected Element Find(string name, int timeoutMS = 3000)
|
||||
protected Element Find(string name, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Session.Find(name, timeoutMS);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,12 +269,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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
|
||||
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.FindAll<T>(by, timeoutMS);
|
||||
return this.Session.FindAll<T>(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -127,12 +283,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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
|
||||
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 5000, bool global = false)
|
||||
where T : Element, new()
|
||||
{
|
||||
return this.Session.FindAll<T>(By.Name(name), timeoutMS);
|
||||
return this.Session.FindAll<T>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -140,11 +296,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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
|
||||
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Session.FindAll<Element>(by, timeoutMS);
|
||||
return this.Session.FindAll<Element>(by, timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,11 +308,121 @@ 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 3000).</param>
|
||||
/// <param name="timeoutMS">The timeout in milliseconds (default is 5000).</param>
|
||||
/// <returns>A read-only collection of the found elements.</returns>
|
||||
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
|
||||
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 5000, bool global = false)
|
||||
{
|
||||
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
|
||||
return this.Session.FindAll<Element>(By.Name(name), timeoutMS, global);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,8 +430,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
{
|
||||
this.sessionHelper.RestartScopeExe();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
|
||||
this.sessionHelper!.RestartScopeExe();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,8 +440,175 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
public void ExitScopeExe()
|
||||
{
|
||||
this.sessionHelper.ExitScopeExe();
|
||||
this.sessionHelper!.ExitScopeExe();
|
||||
return;
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
160
src/common/UITestAutomation/VisualAssert.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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 limiation 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 not 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
170
src/common/UITestAutomation/VisualHelper.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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
|
||||
{
|
||||
#pragma warning disable SA1307
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IMMERSIVE_COLOR_PREFERENCE
|
||||
{
|
||||
public uint dwColorSetIndex;
|
||||
public uint crStartColor;
|
||||
public uint crAccentColor;
|
||||
}
|
||||
#pragma warning restore SA1307
|
||||
|
||||
[DllImport("uxtheme.dll", EntryPoint = "#120")]
|
||||
private static extern IntPtr GetUserColorPreference(ref IMMERSIVE_COLOR_PREFERENCE pcpPreference, bool fForceReload);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system accent color.
|
||||
/// </summary>
|
||||
/// <returns>The system accent color as a Color object.</returns>
|
||||
private static Color GetSystemAccentColor()
|
||||
{
|
||||
IMMERSIVE_COLOR_PREFERENCE colorPreference = default(IMMERSIVE_COLOR_PREFERENCE);
|
||||
GetUserColorPreference(ref colorPreference, true);
|
||||
return ToColor(colorPreference.crStartColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a color value to a Color object.
|
||||
/// </summary>
|
||||
/// <param name="c">The color value.</param>
|
||||
/// <returns>The Color object.</returns>
|
||||
private static Color ToColor(uint c)
|
||||
{
|
||||
int r = (int)(c & 0xFF) % 256;
|
||||
int g = (int)((c >> 8) & 0xFF) % 256;
|
||||
int b = (int)(c >> 16) % 256;
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets HSL values from a Color object.
|
||||
/// </summary>
|
||||
/// <param name="color">The Color object.</param>
|
||||
/// <returns>A tuple containing the HSL values.</returns>
|
||||
private static (double H, double S, double L) GetHSL(Color color)
|
||||
{
|
||||
double rNorm = color.R / 255.0;
|
||||
double gNorm = color.G / 255.0;
|
||||
double bNorm = color.B / 255.0;
|
||||
|
||||
double max = Math.Max(rNorm, Math.Max(gNorm, bNorm));
|
||||
double min = Math.Min(rNorm, Math.Min(gNorm, bNorm));
|
||||
double h = 0, s = 0, l = (max + min) / 2;
|
||||
|
||||
if (max != min)
|
||||
{
|
||||
double delta = max - min;
|
||||
s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
||||
|
||||
if (max == rNorm)
|
||||
{
|
||||
h = ((gNorm - bNorm) / delta) + (gNorm < bNorm ? 6 : 0);
|
||||
}
|
||||
else if (max == gNorm)
|
||||
{
|
||||
h = ((bNorm - rNorm) / delta) + 2;
|
||||
}
|
||||
else if (max == bNorm)
|
||||
{
|
||||
h = ((rNorm - gNorm) / delta) + 4;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return (h * 360, s * 100, l * 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a specific color in an image transparent.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the image file.</param>
|
||||
/// <param name="outputPath">The path to save the output image file.</param>
|
||||
/// <param name="targetColor">The target color to make transparent.</param>
|
||||
/// <param name="fuzz">The fuzz factor for color comparison, default is 2.</param>
|
||||
private static void MakeColorTransparent(string imagePath, string outputPath, Color targetColor, int fuzz = 2)
|
||||
{
|
||||
var hsl = GetHSL(targetColor);
|
||||
|
||||
// Assert.IsNotNull(null, $"Target Color - H: {hsl.H}, S: {hsl.S}, L: {hsl.L}");
|
||||
using (Bitmap originalBitmap = new Bitmap(imagePath))
|
||||
{
|
||||
using (Bitmap bitmap = new Bitmap(originalBitmap))
|
||||
{
|
||||
for (int y = 0; y < bitmap.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < bitmap.Width; x++)
|
||||
{
|
||||
Color pixelColor = bitmap.GetPixel(x, y);
|
||||
if (HueIsSame(pixelColor, targetColor, fuzz))
|
||||
{
|
||||
bitmap.SetPixel(x, y, Color.Transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bitmap.Save(outputPath, ImageFormat.Png);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erases the user preference color from an image. Will overwrite this image.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the image file.</param>
|
||||
/// <param name="fuzz">The fuzz factor for color comparison, default is 2.</param>
|
||||
public static void EraseUserPreferenceColor(string imagePath, int fuzz = 2)
|
||||
{
|
||||
Color systemColor = GetSystemAccentColor();
|
||||
string tempPath = Path.GetTempFileName();
|
||||
MakeColorTransparent(imagePath, tempPath, systemColor, fuzz);
|
||||
File.Delete(imagePath);
|
||||
File.Move(tempPath, imagePath);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the hue of two colors with a fuzz factor.
|
||||
/// </summary>
|
||||
/// <param name="c1">The first color.</param>
|
||||
/// <param name="c2">The second color.</param>
|
||||
/// <param name="fuzz">The fuzz factor, default is 2.</param>
|
||||
/// <returns>True if the hues are the same, otherwise false.</returns>
|
||||
public static bool HueIsSame(Color c1, Color c2, int fuzz = 2)
|
||||
{
|
||||
var h1 = GetHSL(c1).H;
|
||||
var h2 = GetHSL(c2).H;
|
||||
return Math.Abs(h1 - h2) <= fuzz;
|
||||
}
|
||||
}
|
||||
}
|
||||
297
src/common/UITestAutomation/WindowHelper.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
// 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 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");
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,7 @@ namespace package
|
||||
if (!std::filesystem::exists(directoryPath))
|
||||
{
|
||||
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "leilzh@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "leilzh@microsoft.com",
|
||||
|
||||
@@ -18,10 +18,19 @@ public static class OcrHelpers
|
||||
{
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
|
||||
var ocrLanguage = GetOCRLanguage();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
|
||||
OcrEngine ocrEngine;
|
||||
if (ocrLanguage is not null)
|
||||
{
|
||||
ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
|
||||
}
|
||||
else
|
||||
{
|
||||
ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
<ProjectPriFileName>PowerToys.EnvironmentVariablesUILib.pri</ProjectPriFileName>
|
||||
<GenerateLibraryLayout>true</GenerateLibraryLayout>
|
||||
<IsPackable>true</IsPackable>
|
||||
<!-- The default generated file path exceeds the length limit 260 on the build agent. Using a shorter path as a workaround. -->
|
||||
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -56,7 +54,4 @@
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="EnsureCompilerGeneratedFilesOutputPathExists" BeforeTargets="XamlPreCompile">
|
||||
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -58,7 +58,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -99,7 +99,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -140,7 +140,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -158,19 +158,19 @@
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"Castle.Core.dll",
|
||||
"CommunityToolkit.Mvvm.dll",
|
||||
"Hosts.FuzzTests.dll",
|
||||
"Hosts.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll",
|
||||
"Moq.dll",
|
||||
"testhost.dll",
|
||||
"Castle.Core.dll",
|
||||
"System.IO.Abstractions.dll",
|
||||
"CommunityToolkit.Mvvm.dll",
|
||||
"System.IO.Abstractions.TestingHelpers.dll",
|
||||
"TestableIO.System.IO.Abstractions.dll",
|
||||
"TestableIO.System.IO.Abstractions.TestingHelpers.dll",
|
||||
"TestableIO.System.IO.Abstractions.Wrappers.dll"
|
||||
"TestableIO.System.IO.Abstractions.Wrappers.dll",
|
||||
"Testably.Abstractions.FileSystem.Interface.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -13,7 +13,7 @@ namespace Hosts.UITests
|
||||
public class HostModuleTests : UITestBase
|
||||
{
|
||||
public HostModuleTests()
|
||||
: base(PowerToysModule.Hosts)
|
||||
: base(PowerToysModule.Hosts, WindowSize.Small_Vertical)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -31,14 +31,17 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Basic.EmptyViewShouldWork")]
|
||||
[TestCategory("Hosts File Editor #4")]
|
||||
public void TestEmptyView()
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
|
||||
// 'Add an entry' button (only show-up when list is empty) should be visible
|
||||
Assert.IsTrue(this.FindAll<HyperlinkButton>("Add an entry").Count == 1, "'Add an entry' button should be visible in the empty view");
|
||||
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");
|
||||
|
||||
// Click 'Add an entry' from empty-view for adding Host override rule
|
||||
this.Find<HyperlinkButton>("Add an entry").Click();
|
||||
@@ -46,8 +49,10 @@ 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.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");
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,17 +63,20 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Basic.AddEntryButtonShouldWork")]
|
||||
[TestCategory("Hosts File Editor #4")]
|
||||
public void TestAddingEntry()
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0, "Should have no row after removing all");
|
||||
Assert.IsFalse(this.Has<Button>("Delete"), "Should have no row after removing all");
|
||||
|
||||
this.AddEntry("192.168.0.1", "localhost", true);
|
||||
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 1, "Should have one row now");
|
||||
Assert.IsTrue(this.Has<Button>("Delete"), "Should have one row now");
|
||||
|
||||
VisualAssert.AreEqual(this.TestContext, this.Find("Entries"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,7 +90,8 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Basic.CanNotAddMoreThenNighHosts")]
|
||||
[TestCategory("Hosts File Editor #5")]
|
||||
public void TestTooManyHosts()
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
@@ -116,18 +125,48 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Basic.ErrorMessgeShowupIfNotRunAsAdmin")]
|
||||
[TestCategory("Hosts File Editor #8")]
|
||||
public void TestErrorMessageWithNonAdminPermission()
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
if (this.Session.IsElevated == false)
|
||||
{
|
||||
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");
|
||||
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.NoErrorMessgeShowupIfRunAsAdmin")]
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -144,7 +183,8 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Basic.FiltersControlShouldWork")]
|
||||
[TestCategory("Hosts File Editor #6")]
|
||||
public void TestFilterControl()
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
@@ -212,7 +252,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 afer click Filter Button");
|
||||
Assert.IsFalse(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be closed after clicking Filter Button");
|
||||
}
|
||||
|
||||
private void AddEntry(string ip, string host, bool active = true, bool clickAddEntryButton = true)
|
||||
|
||||
@@ -17,6 +17,17 @@
|
||||
<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" />
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Hosts.UITests
|
||||
[TestClass]
|
||||
public class HostsSettingTests : UITestBase
|
||||
{
|
||||
public HostsSettingTests()
|
||||
: base(PowerToysModule.PowerToysSettings, WindowSize.Medium)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test Warning Dialog at startup
|
||||
/// <list type="bullet">
|
||||
@@ -29,7 +34,9 @@ namespace Hosts.UITests
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestMethod("Hosts.Settings.ShowWarningDialogIfRunAsAdmin")]
|
||||
[TestCategory("Hosts File Editor #1")]
|
||||
[TestCategory("Hosts File Editor #9")]
|
||||
public void TestWarningDialog()
|
||||
{
|
||||
this.LaunchFromSetting(showWarning: true);
|
||||
@@ -54,7 +61,7 @@ namespace Hosts.UITests
|
||||
// wait for 500 ms to make sure Hosts File Editor is launched
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
this.Session.Attach(PowerToysModule.Hosts);
|
||||
this.Session.Attach(PowerToysModule.Hosts, WindowSize.Small_Vertical);
|
||||
|
||||
// Should show warning dialog
|
||||
Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
|
||||
@@ -68,7 +75,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.Find<Window>("Hosts File Editor").Close();
|
||||
this.Session.CloseMainWindow();
|
||||
|
||||
// Restore back to PowerToysSettings Session
|
||||
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
@@ -82,7 +89,7 @@ namespace Hosts.UITests
|
||||
Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed");
|
||||
|
||||
// Close Hosts File Editor window
|
||||
this.Session.Find<Window>("Hosts File Editor").Close();
|
||||
this.Session.CloseMainWindow();
|
||||
|
||||
// Restore back to PowerToysSettings Session
|
||||
this.Session.Attach(PowerToysModule.PowerToysSettings);
|
||||
@@ -121,7 +128,7 @@ namespace Hosts.UITests
|
||||
// launch Hosts File Editor
|
||||
this.Find<Button>("Launch Hosts File Editor").Click();
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
Task.Delay(2000).Wait();
|
||||
|
||||
this.Session.Attach(PowerToysModule.Hosts);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
@@ -141,7 +141,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
@@ -153,7 +153,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -4,5 +4,5 @@
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.6.250205002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.7.250401001" targetFramework="native" />
|
||||
</packages>
|
||||
640
src/modules/MouseUtils/MouseUtils.UITests/FindMyMouseTests.cs
Normal file
@@ -0,0 +1,640 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
// 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;
|
||||
|
||||
var location = Session.GetMousePosition();
|
||||
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
|
||||
var colorLeftClick = GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick);
|
||||
|
||||
var colorLeftClick2 = GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick2);
|
||||
|
||||
var colorBackground = 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 = GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick);
|
||||
|
||||
colorLeftClick2 = GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
|
||||
Assert.AreEqual(expectedColor, colorLeftClick2);
|
||||
|
||||
colorBackground = 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 = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
238
src/modules/MouseUtils/MouseUtils.UITests/MouseJumpTests.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Click on the Mouse utilities
|
||||
// Task.Delay(2000).Wait();
|
||||
if (firstTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
@@ -0,0 +1,64 @@
|
||||
## [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 shorctut 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.
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
285
src/modules/MouseUtils/MouseUtils.UITests/util/IOUtil.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
@@ -121,6 +121,22 @@ namespace AppLauncher
|
||||
// packaged apps: try launching first by AppUserModel.ID
|
||||
// usage example: elevated Terminal
|
||||
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
if (res.isOk())
|
||||
{
|
||||
launched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
|
||||
}
|
||||
}
|
||||
|
||||
// win32 app with appUserModelId:
|
||||
// usage example: steam games
|
||||
if (!launched && !app.appUserModelId.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "AppUtils.h"
|
||||
#include "SteamHelper.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <propvarutil.h>
|
||||
@@ -34,6 +35,8 @@ namespace Utils
|
||||
|
||||
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
|
||||
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
|
||||
|
||||
constexpr const wchar_t* SteamUrlProtocol = L"steam:";
|
||||
}
|
||||
|
||||
AppList IterateAppsFolder()
|
||||
@@ -138,6 +141,34 @@ namespace Utils
|
||||
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
|
||||
{
|
||||
data.installPath = propVariantString.m_pData;
|
||||
|
||||
if (!data.installPath.empty())
|
||||
{
|
||||
const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
|
||||
if (isSteamProtocol)
|
||||
{
|
||||
Logger::info(L"Found steam game: protocol path: {}", data.installPath);
|
||||
data.protocolPath = data.installPath;
|
||||
|
||||
try
|
||||
{
|
||||
auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
|
||||
auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
|
||||
|
||||
if (gameFolder)
|
||||
{
|
||||
data.installPath = gameFolder->gameInstallationPath;
|
||||
Logger::info(L"Found steam game: physical path: {}", data.installPath);
|
||||
}
|
||||
}
|
||||
catch (std::exception ex)
|
||||
{
|
||||
Logger::error(L"Failed to get installPath for game {}", data.installPath);
|
||||
Logger::error("Error: {}", ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,5 +428,10 @@ namespace Utils
|
||||
{
|
||||
return installPath.ends_with(NonLocalizable::ChromeFilename);
|
||||
}
|
||||
|
||||
bool AppData::IsSteamGame() const
|
||||
{
|
||||
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ namespace Utils
|
||||
std::wstring packageFullName;
|
||||
std::wstring appUserModelId;
|
||||
std::wstring pwaAppId;
|
||||
std::wstring protocolPath;
|
||||
bool canLaunchElevated = false;
|
||||
|
||||
bool IsEdge() const;
|
||||
bool IsChrome() const;
|
||||
bool IsSteamGame() const;
|
||||
};
|
||||
|
||||
using AppList = std::vector<AppData>;
|
||||
|
||||
171
src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "pch.h"
|
||||
#include "SteamHelper.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
|
||||
static std::wstring Utf8ToWide(const std::string& utf8)
|
||||
{
|
||||
if (utf8.empty())
|
||||
return L"";
|
||||
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
|
||||
if (size <= 0)
|
||||
return L"";
|
||||
|
||||
std::wstring wide(size, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), size);
|
||||
return wide;
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
using namespace std;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::optional<std::wstring> GetSteamExePathFromRegistry()
|
||||
{
|
||||
static std::optional<std::wstring> cachedPath;
|
||||
if (cachedPath.has_value())
|
||||
{
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const std::vector<HKEY> roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
|
||||
const std::vector<std::wstring> subKeys = {
|
||||
L"steam\\shell\\open\\command",
|
||||
L"Software\\Classes\\steam\\shell\\open\\command",
|
||||
};
|
||||
|
||||
for (HKEY root : roots)
|
||||
{
|
||||
for (const auto& subKey : subKeys)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
wchar_t value[512];
|
||||
DWORD size = sizeof(value);
|
||||
DWORD type = 0;
|
||||
|
||||
if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast<LPBYTE>(value), &size) == ERROR_SUCCESS &&
|
||||
(type == REG_SZ || type == REG_EXPAND_SZ))
|
||||
{
|
||||
std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
|
||||
std::wcmatch match;
|
||||
if (std::regex_search(value, match, exeRegex) && match.size() > 1)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
cachedPath = match[1].str();
|
||||
return cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedPath = std::nullopt;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static fs::path GetSteamBasePath()
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
|
||||
}
|
||||
|
||||
static fs::path GetAcfFilePath(const std::wstring& gameId)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
|
||||
}
|
||||
|
||||
static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / L"common" / gameFolderName;
|
||||
}
|
||||
|
||||
static unordered_map<wstring, wstring> ParseAcfFile(const fs::path& acfPath)
|
||||
{
|
||||
unordered_map<wstring, wstring> result;
|
||||
|
||||
ifstream file(acfPath);
|
||||
if (!file.is_open())
|
||||
return result;
|
||||
|
||||
string line;
|
||||
while (getline(file, line))
|
||||
{
|
||||
smatch matches;
|
||||
static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
|
||||
|
||||
if (regex_search(line, matches, pattern) && matches.size() == 3)
|
||||
{
|
||||
wstring key = Utf8ToWide(matches[1].str());
|
||||
wstring value = Utf8ToWide(matches[2].str());
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Steam::SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
|
||||
{
|
||||
fs::path acfPath = Steam::GetAcfFilePath(gameId);
|
||||
|
||||
if (!fs::exists(acfPath))
|
||||
return nullptr;
|
||||
|
||||
auto kv = ParseAcfFile(acfPath);
|
||||
if (kv.empty() || kv.find(L"installdir") == kv.end())
|
||||
return nullptr;
|
||||
|
||||
fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
|
||||
if (!fs::exists(gamePath))
|
||||
return nullptr;
|
||||
|
||||
auto game = std::make_unique<Steam::SteamGame>();
|
||||
game->gameId = gameId;
|
||||
game->gameInstallationPath = gamePath.wstring();
|
||||
return game;
|
||||
}
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
|
||||
{
|
||||
const std::wstring steamGamePrefix = L"steam://rungameid/";
|
||||
|
||||
if (urlPath.rfind(steamGamePrefix, 0) == 0)
|
||||
{
|
||||
return urlPath.substr(steamGamePrefix.length());
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring AcfFileNameTemplate = L"appmanifest_<gameid>.acfs";
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
struct SteamGame
|
||||
{
|
||||
std::wstring gameId;
|
||||
std::wstring gameInstallationPath;
|
||||
};
|
||||
|
||||
std::unique_ptr<SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PwaHelper.h" />
|
||||
<ClInclude Include="Result.h" />
|
||||
<ClInclude Include="SteamHelper.h" />
|
||||
<ClInclude Include="StringUtils.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="WbemHelper.h" />
|
||||
@@ -57,6 +58,7 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PwaHelper.cpp" />
|
||||
<ClCompile Include="SteamGameHelper.cpp" />
|
||||
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
|
||||
<ClCompile Include="WbemHelper.cpp" />
|
||||
<ClCompile Include="WorkspacesData.cpp" />
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<ClInclude Include="StringUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SteamHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
@@ -88,6 +91,9 @@
|
||||
<ClCompile Include="WbemHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SteamGameHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -71,6 +71,8 @@ namespace SnapshotUtils
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info("Try to get window app:{}", reinterpret_cast<void*>(window));
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
|
||||
@@ -118,10 +120,19 @@ namespace SnapshotUtils
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
|
||||
if (!data.has_value() || data->name.empty())
|
||||
{
|
||||
Logger::info(L"Installed app not found: {}", processPath);
|
||||
Logger::info(L"Installed app not found:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info(L"Found app for window:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
|
||||
auto appData = data.value();
|
||||
|
||||
bool isEdge = appData.IsEdge();
|
||||
|
||||
@@ -200,6 +200,14 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
|
||||
}
|
||||
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data.has_value())
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -9,10 +9,12 @@ namespace WindowFilter
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
|
||||
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
|
||||
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
|
||||
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
|
||||
|
||||
Logger::info("Style for window: {}, {:#x}", reinterpret_cast<void*>(window), style);
|
||||
|
||||
if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
|
||||
{
|
||||
// popup windows we want to snap: e.g. Calculator, Telegram
|
||||
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
|
||||
|
||||
@@ -121,4 +121,11 @@ namespace WindowUtils
|
||||
|
||||
return std::wstring(title);
|
||||
}
|
||||
|
||||
|
||||
inline bool HasThickFrame(HWND window)
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<Project DefaultTargets="Build"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
@@ -49,13 +50,20 @@
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>
|
||||
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
|
||||
%(PreprocessorDefinitions);
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
|
||||
IS_DEV_BRANDING;%(PreprocessorDefinitions)
|
||||
</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
@@ -10,10 +11,11 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/package.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
#include <Psapi.h>
|
||||
#include <TlHelp32.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <thread>
|
||||
|
||||
HINSTANCE g_hInst_cmdPal = 0;
|
||||
|
||||
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
|
||||
class CmdPal : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
|
||||
std::wstring app_name;
|
||||
|
||||
//contains the non localized key of the powertoy
|
||||
@@ -46,7 +46,10 @@ private:
|
||||
|
||||
HANDLE m_hTerminateEvent;
|
||||
|
||||
void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
|
||||
// Track if this is the first call to enable
|
||||
bool firstEnableCall = true;
|
||||
|
||||
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
|
||||
{
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
|
||||
@@ -54,6 +57,10 @@ private:
|
||||
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
||||
sei.hwnd = nullptr;
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
|
||||
if (silentFail)
|
||||
{
|
||||
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
|
||||
}
|
||||
sei.lpVerb = elevated ? L"runas" : L"open";
|
||||
sei.lpFile = appPath.c_str();
|
||||
sei.lpParameters = commandLineArgs.c_str();
|
||||
@@ -64,7 +71,11 @@ private:
|
||||
{
|
||||
std::wstring error = get_last_error_or_default(GetLastError());
|
||||
Logger::error(L"Failed to launch process. {}", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_launched.store(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
|
||||
@@ -122,6 +133,9 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
static std::atomic<bool> m_enabled;
|
||||
static std::atomic<bool> m_launched;
|
||||
|
||||
CmdPal()
|
||||
{
|
||||
app_name = L"CmdPal";
|
||||
@@ -133,10 +147,7 @@ public:
|
||||
|
||||
~CmdPal()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
}
|
||||
m_enabled = false;
|
||||
CmdPal::m_enabled.store(false);
|
||||
}
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
@@ -203,15 +214,17 @@ public:
|
||||
{
|
||||
Logger::trace("CmdPal::enable()");
|
||||
|
||||
m_enabled = true;
|
||||
CmdPal::m_enabled.store(true);
|
||||
|
||||
try
|
||||
{
|
||||
std::wstring packageName = L"Microsoft.CommandPalette";
|
||||
#ifdef _DEBUG
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
std::wstring packageName = L"Microsoft.CommandPalette";
|
||||
std::wstring launchPath = L"x-cmdpal://background";
|
||||
#ifdef IS_DEV_BRANDING
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
#endif
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger::info(L"CmdPal not installed. Installing...");
|
||||
|
||||
@@ -238,19 +251,34 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
#if _DEBUG
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
#else
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
#endif
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
{
|
||||
Logger::error("Cmdpal is not registered, quit..");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstEnableCall)
|
||||
{
|
||||
Logger::trace("Not first attempt, try to launch");
|
||||
LaunchApp(launchPath, L"", false /*no elevated*/, false /*error pop up*/);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If first time enable, do retry launch.
|
||||
Logger::trace("First attempt, try to launch");
|
||||
std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
|
||||
launchThread.detach();
|
||||
}
|
||||
|
||||
firstEnableCall = false;
|
||||
}
|
||||
|
||||
virtual void disable()
|
||||
@@ -258,7 +286,44 @@ public:
|
||||
Logger::trace("CmdPal::disable()");
|
||||
TerminateCmdPal();
|
||||
|
||||
m_enabled = false;
|
||||
CmdPal::m_enabled.store(false);
|
||||
}
|
||||
|
||||
static void RetryLaunch(std::wstring path)
|
||||
{
|
||||
const int base_delay_milliseconds = 1000;
|
||||
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
auto launch_result = LaunchApp(path, L"", false, retry < max_retry);
|
||||
if (launch_result)
|
||||
{
|
||||
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
|
||||
}
|
||||
|
||||
// When we got max retry, we don't need to wait for the next retry.
|
||||
if (retry < max_retry)
|
||||
{
|
||||
int delay = base_delay_milliseconds * (1 << (retry));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
|
||||
}
|
||||
++retry;
|
||||
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
|
||||
|
||||
if (!m_enabled.load() || m_launched.load())
|
||||
{
|
||||
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool on_hotkey(size_t) override
|
||||
@@ -273,11 +338,14 @@ public:
|
||||
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
return CmdPal::m_enabled.load();
|
||||
}
|
||||
};
|
||||
|
||||
std::atomic<bool> CmdPal::m_enabled{ false };
|
||||
std::atomic<bool> CmdPal::m_launched{ false };
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new CmdPal();
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.1.0" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.3" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,11 +14,12 @@ namespace TemplateCmdPalExtension;
|
||||
public class Program
|
||||
{
|
||||
[MTAThread]
|
||||
public static async Task Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
|
||||
{
|
||||
await using global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
@@ -31,6 +32,8 @@ public class Program
|
||||
// This will make the main thread wait until the event is signalled by the extension class.
|
||||
// Since we have single instance of the extension object, we exit as soon as it is disposed.
|
||||
extensionDisposedEvent.WaitOne();
|
||||
server.Stop();
|
||||
server.UnsafeDispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,14 +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.CmdPal.Common.Contracts;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
T Read<T>(string folderPath, string fileName);
|
||||
|
||||
void Save<T>(string folderPath, string fileName, T content);
|
||||
|
||||
void Delete(string folderPath, string fileName);
|
||||
}
|
||||