Compare commits
170 Commits
dev/crutka
...
dev/xiaofe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d421ea7cd9 | ||
|
|
b76b37fbff | ||
|
|
7396ea0c47 | ||
|
|
c91ef00113 | ||
|
|
30e32a895a | ||
|
|
98ef617338 | ||
|
|
195e63393c | ||
|
|
ba4f8c4db8 | ||
|
|
1b8decc26f | ||
|
|
bf533387e8 | ||
|
|
03aa5b9db8 | ||
|
|
c8ed1a4123 | ||
|
|
b8f2ba5695 | ||
|
|
27f27d6c39 | ||
|
|
ea84438a2b | ||
|
|
39b75d6449 | ||
|
|
4d6f19a044 | ||
|
|
b7199abab6 | ||
|
|
693ab67831 | ||
|
|
f52c507067 | ||
|
|
c6a4ee1441 | ||
|
|
d875088d1a | ||
|
|
89c33fae68 | ||
|
|
940165d9fd | ||
|
|
e56d0a3bd1 | ||
|
|
276bde8e3e | ||
|
|
c9922302e5 | ||
|
|
5feda6cc9d | ||
|
|
498ef676f4 | ||
|
|
ff9aab3003 | ||
|
|
cbc519ee1a | ||
|
|
3821e8427c | ||
|
|
86557f27fd | ||
|
|
220ddffb66 | ||
|
|
7e2e5f5d71 | ||
|
|
2eeaf6bc44 | ||
|
|
3eec619b98 | ||
|
|
2c68a29e20 | ||
|
|
1568d028b0 | ||
|
|
8659ae12bf | ||
|
|
964e5c9d5e | ||
|
|
48d9d19df1 | ||
|
|
d21b7fac7b | ||
|
|
39be3e11a6 | ||
|
|
2cc3da5020 | ||
|
|
6916c9fa5a | ||
|
|
d2f17ca848 | ||
|
|
02a9269435 | ||
|
|
f642720087 | ||
|
|
5118454db3 | ||
|
|
d5e3899472 | ||
|
|
8384219232 | ||
|
|
8291e2e4e0 | ||
|
|
52d9822fac | ||
|
|
53b989857b | ||
|
|
f65a4495b0 | ||
|
|
718e725571 | ||
|
|
fd9641cdc5 | ||
|
|
147c0d3ec5 | ||
|
|
a804bf86a4 | ||
|
|
e1316e6631 | ||
|
|
4d1dbea5d1 | ||
|
|
46aa43cb69 | ||
|
|
01359c4b0c | ||
|
|
508b95cf5b | ||
|
|
5946b98f87 | ||
|
|
e61e5b6d83 | ||
|
|
67272ad073 | ||
|
|
a79126373f | ||
|
|
3688995511 | ||
|
|
255a11df85 | ||
|
|
d713a3001d | ||
|
|
614af1dd77 | ||
|
|
90a489352b | ||
|
|
c2dddab4ac | ||
|
|
1eb60302df | ||
|
|
11781d11e9 | ||
|
|
1a14dce5a3 | ||
|
|
211bb15be0 | ||
|
|
2752b217e5 | ||
|
|
b0afc57e77 | ||
|
|
2eb51a1861 | ||
|
|
91077c71a9 | ||
|
|
997622fac8 | ||
|
|
ddb9d8ecc6 | ||
|
|
856cbaa293 | ||
|
|
4067be17b2 | ||
|
|
b4020c6720 | ||
|
|
f87c1bc448 | ||
|
|
f49daa24e0 | ||
|
|
c1c9720992 | ||
|
|
750bb429c5 | ||
|
|
7f5aaf26dc | ||
|
|
af8d5402c8 | ||
|
|
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 |
15
.github/actions/spell-check/expect.txt
vendored
@@ -189,6 +189,7 @@ CImage
|
||||
cla
|
||||
CLASSDC
|
||||
CLASSNOTAVAILABLE
|
||||
cleanmgr
|
||||
clickable
|
||||
clickonce
|
||||
CLIENTEDGE
|
||||
@@ -273,6 +274,7 @@ currentculture
|
||||
CURRENTDIR
|
||||
CURSORINFO
|
||||
cursorpos
|
||||
CURSORSHOWING
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
@@ -341,7 +343,9 @@ DESELECTOTHERS
|
||||
DESIGNINFO
|
||||
DESKTOPABSOLUTEEDITING
|
||||
DESKTOPABSOLUTEPARSING
|
||||
DESKTOPHORZRES
|
||||
desktopshorcutinstalled
|
||||
DESKTOPVERTRES
|
||||
devblogs
|
||||
devdocs
|
||||
devmgmt
|
||||
@@ -352,6 +356,7 @@ DFX
|
||||
DIALOGEX
|
||||
digicert
|
||||
dimm
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
diskmgmt
|
||||
@@ -395,6 +400,7 @@ DVASPECTINFO
|
||||
DVD
|
||||
dvr
|
||||
DVTARGETDEVICE
|
||||
dwflags
|
||||
dwl
|
||||
dwm
|
||||
dwmapi
|
||||
@@ -482,6 +488,7 @@ FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FARPROC
|
||||
fff
|
||||
FFFF
|
||||
FILEEXPLORER
|
||||
FILEFLAGS
|
||||
FILEFLAGSMASK
|
||||
@@ -600,6 +607,7 @@ helptext
|
||||
HGFE
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
hhx
|
||||
Hiber
|
||||
Hiberboot
|
||||
@@ -608,6 +616,7 @@ hicon
|
||||
HIDEREADONLY
|
||||
HIDEWINDOW
|
||||
Hif
|
||||
hightlight
|
||||
HIMAGELIST
|
||||
himl
|
||||
hinst
|
||||
@@ -745,6 +754,7 @@ isocpp
|
||||
iss
|
||||
issecret
|
||||
ISSEPARATOR
|
||||
istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
@@ -1272,6 +1282,7 @@ prvpane
|
||||
psapi
|
||||
pscid
|
||||
PSECURITY
|
||||
psexec
|
||||
psfgao
|
||||
psfi
|
||||
PSMODULEPATH
|
||||
@@ -1533,6 +1544,7 @@ SLGP
|
||||
sln
|
||||
SMALLICON
|
||||
smartphone
|
||||
smileys
|
||||
SMTO
|
||||
SNAPPROCESS
|
||||
snk
|
||||
@@ -1875,6 +1887,7 @@ WINDOWPOSCHANGING
|
||||
WINDOWSBUILDNUMBER
|
||||
windowssearch
|
||||
windowssettings
|
||||
windowsterminal
|
||||
WINDOWSTYLES
|
||||
WINDOWSTYLESICON
|
||||
winerror
|
||||
@@ -1984,6 +1997,7 @@ Zoneszonabletester
|
||||
Zoomin
|
||||
zoomit
|
||||
ZOOMITX
|
||||
Zorder
|
||||
ZXk
|
||||
ZXNs
|
||||
zzz
|
||||
@@ -1995,3 +2009,4 @@ culori
|
||||
Evercoder
|
||||
LCh
|
||||
CIELCh
|
||||
CLSCTXINPROCALL
|
||||
@@ -126,16 +126,15 @@ Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
}
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
|
||||
if (Test-Path $propsFile) {
|
||||
$file = Read-FileWithEncoding -Path $propsFile
|
||||
Get-ChildItem -Path $rootPath -Recurse "Directory.Packages.props" | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$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
|
||||
Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $propsFile
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +143,8 @@ 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]*\\'
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.(?=[-.0-9a-zA-Z]*\d)[-.0-9a-zA-Z]*' #positive lookahead for at least a digit
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
|
||||
@@ -53,6 +53,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
|
||||
@@ -530,7 +533,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,17 @@ 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\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
${{ else }}:
|
||||
testAssemblyVer2: |
|
||||
**\UITests-FancyZonesEditor.dll
|
||||
!**\obj\**
|
||||
!**\ref\**
|
||||
!**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll
|
||||
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
|
||||
@@ -1,7 +1,28 @@
|
||||
$VSInstances = ([xml](& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -include packages -format xml))
|
||||
$VSPackages = $VSInstances.instances.instance.packages.package
|
||||
$LatestVCPackage = ($VSInstances.instances.instance.packages.package | ? { $_.id -eq "Microsoft.VisualCpp.Tools.Core" })
|
||||
$LatestVCPackage = ($VSPackages | ? { $_.id -eq "Microsoft.VisualCpp.Tools.Core" })
|
||||
$LatestVCToolsVersion = $LatestVCPackage.version;
|
||||
|
||||
$VSRoot = (& 'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property 'resolvedInstallationPath')
|
||||
$VCToolsRoot = Join-Path $VSRoot "VC\Tools\MSVC"
|
||||
|
||||
# We have observed a few instances where the VC tools package version actually
|
||||
# differs from the version on the files themselves. We might as well check
|
||||
# whether the version we just found _actually exists_ before we use it.
|
||||
# We'll use whichever highest version exists.
|
||||
$PackageVCToolPath = Join-Path $VCToolsRoot $LatestVCToolsVersion
|
||||
If ($Null -Eq (Get-Item $PackageVCToolPath -ErrorAction:Ignore)) {
|
||||
$VCToolsVersions = Get-ChildItem $VCToolsRoot | ForEach-Object {
|
||||
[Version]$_.Name
|
||||
} | Sort -Descending
|
||||
$LatestActualVCToolsVersion = $VCToolsVersions | Select -First 1
|
||||
|
||||
If ([Version]$LatestVCToolsVersion -Ne $LatestActualVCToolsVersion) {
|
||||
Write-Output "VC Tools Mismatch: Directory = $LatestActualVCToolsVersion, Package = $LatestVCToolsVersion"
|
||||
$LatestVCToolsVersion = $LatestActualVCToolsVersion.ToString(3)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Latest VCToolsVersion: $LatestVCToolsVersion"
|
||||
Write-Output "Updating VCToolsVersion environment variable for job"
|
||||
Write-Output "##vso[task.setvariable variable=VCToolsVersion]$LatestVCToolsVersion"
|
||||
|
||||
@@ -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
|
||||
@@ -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.7.250401001" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
<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" />
|
||||
|
||||
@@ -1472,7 +1472,7 @@ SOFTWARE.
|
||||
- Microsoft.Windows.CsWin32 0.2.46-beta
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
|
||||
- Microsoft.WindowsAppSDK 1.7.250401001
|
||||
- Microsoft.WindowsAppSDK 1.7.250513003
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
|
||||
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
|
||||
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
|
||||
|
||||
@@ -706,10 +706,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZones.FuzzTests", "src\modules\fancyzones\FancyZones.FuzzTests\FancyZones.FuzzTests.csproj", "{0217E86E-3476-9946-DE8E-9D200CEBD47A}"
|
||||
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
|
||||
@@ -2582,6 +2588,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
|
||||
@@ -2596,6 +2618,14 @@ Global
|
||||
{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
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -2735,7 +2765,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}
|
||||
@@ -2866,6 +2896,9 @@ Global
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
|
||||
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {322566EF-20DC-43A6-B9F8-616AF942579A}
|
||||
{43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF}
|
||||
{0217E86E-3476-9946-DE8E-9D200CEBD47A} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
|
||||
{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ImportGroup Label="ExtensionTargetsAppSdkCppProp">
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImportsWinAppSdkCppProp" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,5 +0,0 @@
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<Import Project="$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(MsbuildThisFileDirectory)\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
</Project>
|
||||
@@ -92,6 +92,9 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
var run = 0;
|
||||
var chunksLen = fileSize - QOI_PADDING_LENGTH;
|
||||
|
||||
var x = 0;
|
||||
var rowAdd = bitmapData.Stride - (channels * bitmapData.Width);
|
||||
|
||||
for (var dataIndex = 0; dataIndex < dataLength; dataIndex += channels)
|
||||
{
|
||||
if (run > 0)
|
||||
@@ -153,6 +156,14 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
||||
bitmapPixel[3] = pixel.A;
|
||||
}
|
||||
}
|
||||
|
||||
x++;
|
||||
if (x == bitmapData.Width)
|
||||
{
|
||||
// We align dataIndex with the stride
|
||||
dataIndex += rowAdd;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
|
||||
@@ -6,9 +6,10 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace ManagedCommon
|
||||
@@ -40,25 +41,72 @@ namespace ManagedCommon
|
||||
/// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
|
||||
public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
|
||||
{
|
||||
string basePath;
|
||||
if (isLocalLow)
|
||||
{
|
||||
applicationLogPath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath + "\\" + Version;
|
||||
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
applicationLogPath = Constants.AppDataPath() + applicationLogPath + "\\" + Version;
|
||||
basePath = Constants.AppDataPath() + applicationLogPath;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(applicationLogPath))
|
||||
string versionedPath = Path.Combine(basePath, Version);
|
||||
|
||||
if (!Directory.Exists(versionedPath))
|
||||
{
|
||||
Directory.CreateDirectory(applicationLogPath);
|
||||
Directory.CreateDirectory(versionedPath);
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(applicationLogPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
|
||||
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
|
||||
|
||||
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
|
||||
|
||||
Trace.AutoFlush = true;
|
||||
|
||||
// Clean up old version log folders
|
||||
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old version log folders, keeping only the current version's folder.
|
||||
/// </summary>
|
||||
/// <param name="basePath">The base path to the log files folder.</param>
|
||||
/// <param name="currentVersionPath">The path to the current version's log folder.</param>
|
||||
private static void DeleteOldVersionLogFolders(string basePath, string currentVersionPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dirs = Directory.GetDirectories(basePath)
|
||||
.Select(d => new DirectoryInfo(d))
|
||||
.OrderBy(d => d.CreationTime)
|
||||
.Where(d => !string.Equals(d.FullName, currentVersionPath, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(3)
|
||||
.ToList();
|
||||
|
||||
foreach (var directory in dirs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(directory.FullName, true);
|
||||
LogInfo($"Deleted old log directory: {directory.FullName}");
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError($"Failed to delete old log directory: {directory.FullName}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError("Error cleaning up old log folders", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
|
||||
53
src/common/UITestAutomation/Element/CheckBox.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
public class CheckBox : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.CheckBox";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CheckBox"/> class.
|
||||
/// </summary>
|
||||
public CheckBox()
|
||||
{
|
||||
this.TargetControlType = CheckBox.ExpectedControlType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select the item of the ComboBox.
|
||||
/// </summary>
|
||||
/// <param name="value">The text to select from the list view.</param>
|
||||
public void Select(string value)
|
||||
{
|
||||
this.Find<NavigationViewItem>(value).Click();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the CheckBox is checked.
|
||||
/// </summary>
|
||||
public bool IsChecked => this.Selected;
|
||||
|
||||
public CheckBox SetCheck(bool value = true, int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
if (this.IsChecked != value)
|
||||
{
|
||||
if (msPreAction > 0)
|
||||
{
|
||||
Task.Delay(msPreAction).Wait();
|
||||
}
|
||||
|
||||
// Toggle the switch
|
||||
this.Click();
|
||||
if (msPostAction > 0)
|
||||
{
|
||||
Task.Delay(msPostAction).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
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,12 @@ 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)
|
||||
/// <param name="msPreAction">Delay in milliseconds before performing the click action. Default is 500 ms.</param>
|
||||
/// <param name="msPostAction">Delay in milliseconds after performing the click action. Default is 500 ms.</param>
|
||||
public virtual void Click(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
PerformAction(
|
||||
(actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement);
|
||||
|
||||
@@ -131,7 +147,9 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
});
|
||||
},
|
||||
msPreAction,
|
||||
msPostAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,51 +170,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 +205,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 +213,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 +226,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 +239,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 +251,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 +263,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 +295,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 +308,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 +320,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 +359,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 SaveToPngFile 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 center of the ListItem element.
|
||||
/// </summary>
|
||||
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
|
||||
/// <param name="msPreAction">Pre action delay in milliseconds. Default value is 500</param>
|
||||
/// <param name="msPostAction">Post action delay in milliseconds. Default value is 500</param>
|
||||
public void ClickCenter(bool rightClick = false, int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
PerformAction(
|
||||
(actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement);
|
||||
if (rightClick)
|
||||
{
|
||||
actions.ContextClick();
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Click();
|
||||
}
|
||||
|
||||
actions.Build().Perform();
|
||||
},
|
||||
msPreAction,
|
||||
msPostAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
101
src/common/UITestAutomation/Element/Pane.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
public class Pane : Element
|
||||
{
|
||||
private static readonly string ExpectedControlType = "ControlType.Pane";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pane"/> class.
|
||||
/// </summary>
|
||||
public Pane()
|
||||
{
|
||||
this.TargetControlType = Pane.ExpectedControlType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drag element move offset.
|
||||
/// </summary>
|
||||
/// <param name="offsetX">The offsetX to move.</param>
|
||||
/// <param name="offsetY">The offsetY to move.</param>
|
||||
public void Drag(int offsetX, int offsetY)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY).Release();
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates holding when dragging to target position.
|
||||
/// </summary>
|
||||
/// <param name="offsetX">The offsetX to move.</param>
|
||||
/// <param name="offsetY">The offsetY to move.</param>
|
||||
public void DragAndHold(int offsetX, int offsetY)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
actions.MoveToElement(windowElement).MoveByOffset(10, 10).ClickAndHold(windowElement).MoveByOffset(offsetX, offsetY);
|
||||
actions.Build().Perform();
|
||||
});
|
||||
}
|
||||
|
||||
public void ReleaseDrag()
|
||||
{
|
||||
var releaseAction = new Actions(this.Driver);
|
||||
releaseAction.Release().Perform();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
|
||||
/// </summary>
|
||||
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
|
||||
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
|
||||
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
|
||||
public void KeyDownAndDrag(Key key, int targetX, int targetY)
|
||||
{
|
||||
HoldShiftToDrag(key, targetX, targetY);
|
||||
ReleaseAction();
|
||||
ReleaseKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates holding a key, clicking and dragging a UI element to the specified screen coordinates.
|
||||
/// </summary>
|
||||
/// <param name="key">The keyboard key to press and hold during the drag operation.</param>
|
||||
/// <param name="targetX">The target X-coordinate to drag the element to.</param>
|
||||
/// <param name="targetY">The target Y-coordinate to drag the element to.</param>
|
||||
public void HoldShiftToDrag(Key key, int targetX, int targetY)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
KeyboardHelper.PressKey(key);
|
||||
|
||||
actions.MoveToElement(WindowsElement)
|
||||
.ClickAndHold()
|
||||
.Perform();
|
||||
|
||||
int dx = targetX - windowElement.Rect.X;
|
||||
int dy = targetY - windowElement.Rect.Y;
|
||||
|
||||
int stepCount = 10;
|
||||
int stepX = dx / stepCount;
|
||||
int stepY = dy / stepCount;
|
||||
|
||||
for (int i = 0; i < stepCount; i++)
|
||||
{
|
||||
var stepAction = new Actions(Driver);
|
||||
stepAction.MoveByOffset(stepX, stepY).Perform();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
var items = FindElementsWithRetry(findElementsFunc, timeoutMS);
|
||||
var res = items.Select(item =>
|
||||
{
|
||||
var element = item as WindowsElement;
|
||||
@@ -30,17 +30,30 @@ namespace Microsoft.PowerToys.UITest
|
||||
return new ReadOnlyCollection<T>(res);
|
||||
}
|
||||
|
||||
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
where T : Element, new()
|
||||
private static ReadOnlyCollection<TW> FindElementsWithRetry<TW>(Func<IReadOnlyCollection<TW>> findElementsFunc, int timeoutMS)
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
var res = items.Select(item =>
|
||||
{
|
||||
var element = item as WindowsElement;
|
||||
return NewElement<T>(element, driver, timeoutMS);
|
||||
}).Where(item => item.IsMatchingTarget()).ToList();
|
||||
var timeout = TimeSpan.FromMilliseconds(timeoutMS);
|
||||
var retryIntervalMS = TimeSpan.FromMilliseconds(500);
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
return new ReadOnlyCollection<T>(res);
|
||||
while (DateTime.Now - startTime < timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = findElementsFunc();
|
||||
if (items.Count > 0)
|
||||
{
|
||||
return new ReadOnlyCollection<TW>((IList<TW>)items);
|
||||
}
|
||||
|
||||
Task.Delay(retryIntervalMS).Wait();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadOnlyCollection<TW>(new List<TW>());
|
||||
}
|
||||
|
||||
public static T NewElement<T>(WindowsElement? element, WindowsDriver<WindowsElement>? driver, int timeoutMS)
|
||||
@@ -50,11 +63,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.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 +20,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> pRoot, WindowsDriver<WindowsElement> pDriver, PowerToysModule scope, WindowSize size)
|
||||
{
|
||||
this.Root = root;
|
||||
this.WindowsDriver = windowsDriver;
|
||||
this.MainWindowHandler = IntPtr.Zero;
|
||||
this.Root = pRoot;
|
||||
this.WindowsDriver = pDriver;
|
||||
this.InitScope = scope;
|
||||
|
||||
if (size != WindowSize.UnSpecified)
|
||||
{
|
||||
// Attach to the scope & reset MainWindowHandler
|
||||
this.Attach(scope, size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 +93,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 +257,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 +270,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 +282,186 @@ 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));
|
||||
}
|
||||
},
|
||||
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,35 +469,148 @@ 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 timeout = TimeSpan.FromMinutes(2);
|
||||
var retryInterval = TimeSpan.FromSeconds(5);
|
||||
DateTime startTime = DateTime.Now;
|
||||
|
||||
List<(IntPtr HWnd, string Title)>? matchingWindows = null;
|
||||
|
||||
while (DateTime.Now - startTime < timeout)
|
||||
{
|
||||
matchingWindows = WindowHelper.ApiHelper.FindDesktopWindowHandler(
|
||||
new[] { windowName, WindowHelper.AdministratorPrefix + windowName });
|
||||
|
||||
if (matchingWindows.Count > 0 && matchingWindows[0].HWnd != IntPtr.Zero)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Task.Delay(retryInterval).Wait();
|
||||
}
|
||||
|
||||
if (matchingWindows == null || matchingWindows.Count == 0 || matchingWindows[0].HWnd == IntPtr.Zero)
|
||||
{
|
||||
Assert.Fail($"Failed to attach. Window '{windowName}' not found after {timeout.TotalSeconds} seconds.");
|
||||
}
|
||||
|
||||
// pick one from matching windows
|
||||
this.MainWindowHandler = matchingWindows[0].HWnd;
|
||||
this.IsElevated = matchingWindows[0].Title.StartsWith(WindowHelper.AdministratorPrefix);
|
||||
|
||||
ApiHelper.SetForegroundWindow(this.MainWindowHandler);
|
||||
|
||||
var hexWindowHandle = this.MainWindowHandler.ToInt64().ToString("x");
|
||||
|
||||
var 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
|
||||
{
|
||||
Assert.IsNotNull(this.Root, $"Failed to attach to the window '{windowName}'. Root driver is null");
|
||||
}
|
||||
|
||||
Task.Delay(3000).Wait();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the main window size.
|
||||
/// </summary>
|
||||
/// <param name="size">WindowSize enum</param>
|
||||
public void SetMainWindowSize(WindowSize size)
|
||||
{
|
||||
if (this.MainWindowHandler == IntPtr.Zero)
|
||||
{
|
||||
// Attach to the scope & reset MainWindowHandler
|
||||
this.Attach(this.InitScope);
|
||||
}
|
||||
|
||||
WindowHelper.SetWindowSize(this.MainWindowHandler, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(x, y)</returns>
|
||||
public (int CenterX, int CenterY) GetMainWindowCenter()
|
||||
{
|
||||
return WindowHelper.GetWindowCenter(this.MainWindowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(int Left, int Top, int Right, int Bottom)</returns>
|
||||
public (int Left, int Top, int Right, int Bottom) GetMainWindowRect()
|
||||
{
|
||||
return WindowHelper.GetWindowRect(this.MainWindowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Launches the specified executable with optional arguments and simulates a delay before and after execution.
|
||||
/// </summary>
|
||||
/// <param name="executablePath">The full path to the executable to launch.</param>
|
||||
/// <param name="arguments">Optional command-line arguments to pass to the executable.</param>
|
||||
/// <param name="msPreAction">The number of milliseconds to wait before launching the executable. Default is 0 ms.</param>
|
||||
/// <param name="msPostAction">The number of milliseconds to wait after launching the executable. Default is 2000 ms.</param>
|
||||
public void StartExe(string executablePath, string arguments = "", int msPreAction = 0, int msPostAction = 2000)
|
||||
{
|
||||
PerformAction(
|
||||
() =>
|
||||
{
|
||||
StartExeInternal(executablePath, arguments);
|
||||
},
|
||||
msPreAction,
|
||||
msPostAction);
|
||||
}
|
||||
|
||||
private void StartExeInternal(string executablePath, string arguments = "")
|
||||
{
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executablePath,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = true,
|
||||
};
|
||||
Process.Start(processInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates all running processes that match the specified process name.
|
||||
/// Waits for each process to exit after sending the kill signal.
|
||||
/// </summary>
|
||||
/// <param name="processName">The name of the process to terminate (without extension, e.g., "notepad").</param>
|
||||
public void KillAllProcessesByName(string processName)
|
||||
{
|
||||
foreach (var process in Process.GetProcessesByName(processName))
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a manual operation on the element.
|
||||
/// </summary>
|
||||
@@ -254,5 +633,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Nested class for test initialization.
|
||||
/// </summary>
|
||||
internal class SessionHelper
|
||||
public class SessionHelper
|
||||
{
|
||||
// Default session path is PowerToys settings dashboard
|
||||
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
|
||||
|
||||
private readonly string runnerPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.Runner);
|
||||
|
||||
private string? locationPath;
|
||||
|
||||
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||
@@ -27,27 +29,34 @@ 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);
|
||||
|
||||
var winAppDriverProcessInfo = new ProcessStartInfo
|
||||
this.StartWindowsAppDriverApp();
|
||||
|
||||
var runnerProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
|
||||
FileName = locationPath + this.runnerPath,
|
||||
Verb = "runas",
|
||||
};
|
||||
|
||||
this.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
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 +65,8 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <param name="scope">The PowerToys module to start.</param>
|
||||
public SessionHelper Init()
|
||||
{
|
||||
this.StartExe(locationPath + this.sessionPath);
|
||||
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 +83,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 +96,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 +120,55 @@ 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);
|
||||
this.Driver = NewWindowsDriver(opts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new exe and takes control of it.
|
||||
/// </summary>
|
||||
/// <param name="info">The path to the application executable.</param>
|
||||
private WindowsDriver<WindowsElement> NewWindowsDriver(AppiumOptions info)
|
||||
{
|
||||
// Create driver with retry
|
||||
TimeSpan timeout = TimeSpan.FromMinutes(2);
|
||||
DateTime startTime = DateTime.Now;
|
||||
int retryCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), info);
|
||||
return res;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (DateTime.Now - startTime > timeout)
|
||||
{
|
||||
Console.WriteLine("Timeout reached. Throwing exception.");
|
||||
throw;
|
||||
}
|
||||
|
||||
TimeSpan retryInterval = retryCount < 2
|
||||
? TimeSpan.FromSeconds(5)
|
||||
: TimeSpan.FromSeconds(20);
|
||||
|
||||
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Exception occurred: {ex.Message}, Retrying after {retryInterval.TotalSeconds} seconds...");
|
||||
|
||||
Task.Delay(retryInterval).Wait();
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exit now exe.
|
||||
/// </summary>
|
||||
@@ -134,7 +182,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
public void RestartScopeExe()
|
||||
{
|
||||
ExitExe(sessionPath);
|
||||
ExitScopeExe();
|
||||
StartExe(locationPath + sessionPath);
|
||||
}
|
||||
|
||||
@@ -145,5 +193,17 @@ namespace Microsoft.PowerToys.UITest
|
||||
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
|
||||
return this.Driver;
|
||||
}
|
||||
|
||||
private void StartWindowsAppDriverApp()
|
||||
{
|
||||
var winAppDriverProcessInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
|
||||
Verb = "runas",
|
||||
};
|
||||
|
||||
this.ExitExe(winAppDriverProcessInfo.FileName);
|
||||
this.appDriver = Process.Start(winAppDriverProcessInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,37 @@ 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 bool IsInPipeline { get; }
|
||||
|
||||
public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List<MonitorInfoData.MonitorInfoDataWrapper>() };
|
||||
|
||||
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)
|
||||
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 +62,22 @@ namespace Microsoft.PowerToys.UITest
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
CloseOtherApplications();
|
||||
if (IsInPipeline)
|
||||
{
|
||||
screenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(screenshotDirectory);
|
||||
|
||||
// Take screenshot every 1 second
|
||||
screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, screenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000));
|
||||
|
||||
// Escape Popups before starting
|
||||
System.Windows.Forms.SendKeys.SendWait("{ESC}");
|
||||
}
|
||||
|
||||
this.sessionHelper = new SessionHelper(scope).Init();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), scope, size);
|
||||
|
||||
if (this.scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
// close Debug warning dialog if any
|
||||
@@ -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,137 @@ 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>
|
||||
/// Scrolls the page
|
||||
/// </summary>
|
||||
/// <param name="scrollCount">The number of scroll attempts.</param>
|
||||
/// <param name="direction">The direction to scroll.</param>
|
||||
/// <param name="msPreAction">Pre-action delay in milliseconds.</param>
|
||||
/// <param name="msPostAction">Post-action delay in milliseconds.</param>
|
||||
public void Scroll(int scrollCount = 5, string direction = "Up", int msPreAction = 500, int msPostAction = 500)
|
||||
{
|
||||
MouseActionType mouseAction = direction == "Up" ? MouseActionType.ScrollUp : MouseActionType.ScrollDown;
|
||||
for (int i = 0; i < scrollCount; i++)
|
||||
{
|
||||
Session.PerformMouseAction(mouseAction, msPreAction, msPostAction); // Ensure settings are visible
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the last screenshot when the test fails.
|
||||
/// </summary>
|
||||
protected void CaptureLastScreenshot()
|
||||
{
|
||||
// Implement your screenshot capture logic here
|
||||
// For example, save a screenshot to a file and return the file path
|
||||
string screenshotPath = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "last_screenshot.png");
|
||||
|
||||
this.Session.Root.GetScreenshot().SaveAsFile(screenshotPath, ScreenshotImageFormat.Png);
|
||||
|
||||
// Save screenshot to screenshotPath & upload to test attachment
|
||||
this.TestContext.AddResultFile(screenshotPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the color of the pixel at the specified screen coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x">The X coordinate on the screen.</param>
|
||||
/// <param name="y">The Y coordinate on the screen.</param>
|
||||
/// <returns>The color of the pixel at the specified coordinates.</returns>
|
||||
public Color GetPixelColor(int x, int y)
|
||||
{
|
||||
return WindowHelper.GetPixelColor(x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the color of the pixel at the specified screen coordinates as a string.
|
||||
/// </summary>
|
||||
/// <param name="x">The X coordinate on the screen.</param>
|
||||
/// <param name="y">The Y coordinate on the screen.</param>
|
||||
/// <returns>The color of the pixel at the specified coordinates.</returns>
|
||||
public string GetPixelColorString(int x, int y)
|
||||
{
|
||||
return WindowHelper.GetPixelColorString(x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the display.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A tuple containing the width and height of the display.
|
||||
/// </returns
|
||||
public Tuple<int, int> GetDisplaySize()
|
||||
{
|
||||
return WindowHelper.GetDisplaySize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a combination of keys.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keys to send.</param>
|
||||
public void SendKeys(params Key[] keys)
|
||||
{
|
||||
this.Session.SendKeys(keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a sequence of keys.
|
||||
/// </summary>
|
||||
/// <param name="keys">An array of keys to send.</param>
|
||||
public void SendKeySequence(params Key[] keys)
|
||||
{
|
||||
this.Session.SendKeySequence(keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current position of the mouse cursor as a tuple.
|
||||
/// </summary>
|
||||
/// <returns>A tuple containing the X and Y coordinates of the cursor.</returns>
|
||||
public Tuple<int, int> GetMousePosition()
|
||||
{
|
||||
return this.Session.GetMousePosition();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the screen center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(x, y)</returns>
|
||||
public (int CenterX, int CenterY) GetScreenCenter()
|
||||
{
|
||||
return WindowHelper.GetScreenCenter();
|
||||
}
|
||||
|
||||
public bool IsWindowOpen(string windowName)
|
||||
{
|
||||
return WindowHelper.IsWindowOpen(windowName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the mouse cursor to the specified screen coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x">The new x-coordinate of the cursor.</param>
|
||||
/// <param name="y">The new y-coordinate of the cursor.</param
|
||||
public void MoveMouseTo(int x, int y)
|
||||
{
|
||||
this.Session.MoveMouseTo(x, y);
|
||||
}
|
||||
|
||||
protected void AddScreenShotsToTestResultsDirectory()
|
||||
{
|
||||
if (screenshotDirectory != null)
|
||||
{
|
||||
foreach (string file in Directory.GetFiles(screenshotDirectory))
|
||||
{
|
||||
this.TestContext.AddResultFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,8 +446,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 +456,194 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
public void ExitScopeExe()
|
||||
{
|
||||
this.sessionHelper.ExitScopeExe();
|
||||
this.sessionHelper!.ExitScopeExe();
|
||||
return;
|
||||
}
|
||||
|
||||
private void CloseOtherApplications()
|
||||
{
|
||||
// Close other applications
|
||||
var processNamesToClose = new List<string>
|
||||
{
|
||||
"PowerToys",
|
||||
"PowerToys.Settings",
|
||||
"PowerToys.FancyZonesEditor",
|
||||
};
|
||||
foreach (var processName in processNamesToClose)
|
||||
{
|
||||
foreach (var process in Process.GetProcessesByName(processName))
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class NativeMethods
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct DISPLAY_DEVICE
|
||||
{
|
||||
public int cb;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string DeviceName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceString;
|
||||
public int StateFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceID;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceKey;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int EnumDisplaySettings(IntPtr deviceName, int modeNum, ref DEVMODE devMode);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern bool EnumDisplayDevices(IntPtr lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, int dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Ansi)]
|
||||
private static extern int ChangeDisplaySettingsEx(IntPtr lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam);
|
||||
|
||||
private const int DM_PELSWIDTH = 0x80000;
|
||||
private const int DM_PELSHEIGHT = 0x100000;
|
||||
|
||||
public const int ENUM_CURRENT_SETTINGS = -1;
|
||||
public const int CDS_TEST = 0x00000002;
|
||||
public const int CDS_UPDATEREGISTRY = 0x01;
|
||||
public const int DISP_CHANGE_SUCCESSFUL = 0;
|
||||
public const int DISP_CHANGE_RESTART = 1;
|
||||
public const int DISP_CHANGE_FAILED = -1;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DEVMODE
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string DmDeviceName;
|
||||
public short DmSpecVersion;
|
||||
public short DmDriverVersion;
|
||||
public short DmSize;
|
||||
public short DmDriverExtra;
|
||||
public int DmFields;
|
||||
public int DmPositionX;
|
||||
public int DmPositionY;
|
||||
public int DmDisplayOrientation;
|
||||
public int DmDisplayFixedOutput;
|
||||
public short DmColor;
|
||||
public short DmDuplex;
|
||||
public short DmYResolution;
|
||||
public short DmTTOption;
|
||||
public short DmCollate;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string DmFormName;
|
||||
public short DmLogPixels;
|
||||
public int DmBitsPerPel;
|
||||
public int DmPelsWidth;
|
||||
public int DmPelsHeight;
|
||||
public int DmDisplayFlags;
|
||||
public int DmDisplayFrequency;
|
||||
public int DmICMMethod;
|
||||
public int DmICMIntent;
|
||||
public int DmMediaType;
|
||||
public int DmDitherType;
|
||||
public int DmReserved1;
|
||||
public int DmReserved2;
|
||||
public int DmPanningWidth;
|
||||
public int DmPanningHeight;
|
||||
}
|
||||
|
||||
public static void GetMonitorInfo()
|
||||
{
|
||||
int deviceIndex = 0;
|
||||
DISPLAY_DEVICE d = default(DISPLAY_DEVICE);
|
||||
d.cb = Marshal.SizeOf(d);
|
||||
|
||||
Console.WriteLine("monitor list :");
|
||||
while (EnumDisplayDevices(IntPtr.Zero, deviceIndex, ref d, 0))
|
||||
{
|
||||
Console.WriteLine($"monitor {deviceIndex + 1}:");
|
||||
Console.WriteLine($" name: {d.DeviceName}");
|
||||
Console.WriteLine($" string: {d.DeviceString}");
|
||||
Console.WriteLine($" ID: {d.DeviceID}");
|
||||
Console.WriteLine($" key: {d.DeviceKey}");
|
||||
Console.WriteLine();
|
||||
|
||||
DEVMODE dm = default(DEVMODE);
|
||||
dm.DmSize = (short)Marshal.SizeOf<DEVMODE>();
|
||||
int modeNum = 0;
|
||||
while (EnumDisplaySettings(d.DeviceName, modeNum, ref dm) > 0)
|
||||
{
|
||||
MonitorInfoData.Monitors.Add(new MonitorInfoData.MonitorInfoDataWrapper()
|
||||
{
|
||||
DeviceName = d.DeviceName,
|
||||
DeviceString = d.DeviceString,
|
||||
DeviceID = d.DeviceID,
|
||||
DeviceKey = d.DeviceKey,
|
||||
PelsWidth = dm.DmPelsWidth,
|
||||
PelsHeight = dm.DmPelsHeight,
|
||||
DisplayFrequency = dm.DmDisplayFrequency,
|
||||
});
|
||||
Console.WriteLine($" mode {modeNum}: {dm.DmPelsWidth}x{dm.DmPelsHeight} @ {dm.DmDisplayFrequency}Hz");
|
||||
modeNum++;
|
||||
}
|
||||
|
||||
deviceIndex++;
|
||||
d.cb = Marshal.SizeOf(d); // Reset the size for the next device
|
||||
}
|
||||
}
|
||||
|
||||
public static void ChangeDisplayResolution(int PelsWidth, int PelsHeight)
|
||||
{
|
||||
Screen screen = Screen.PrimaryScreen!;
|
||||
if (screen.Bounds.Width == PelsWidth && screen.Bounds.Height == PelsHeight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DEVMODE devMode = default(DEVMODE);
|
||||
devMode.DmDeviceName = new string(new char[32]);
|
||||
devMode.DmFormName = new string(new char[32]);
|
||||
devMode.DmSize = (short)Marshal.SizeOf<DEVMODE>();
|
||||
|
||||
int modeNum = 0;
|
||||
while (EnumDisplaySettings(IntPtr.Zero, modeNum, ref devMode) > 0)
|
||||
{
|
||||
Console.WriteLine($"Mode {modeNum}: {devMode.DmPelsWidth}x{devMode.DmPelsHeight} @ {devMode.DmDisplayFrequency}Hz");
|
||||
modeNum++;
|
||||
}
|
||||
|
||||
devMode.DmPelsWidth = PelsWidth;
|
||||
devMode.DmPelsHeight = PelsHeight;
|
||||
|
||||
int result = NativeMethods.ChangeDisplaySettings(ref devMode, NativeMethods.CDS_TEST);
|
||||
|
||||
if (result == DISP_CHANGE_SUCCESSFUL)
|
||||
{
|
||||
result = ChangeDisplaySettings(ref devMode, CDS_UPDATEREGISTRY);
|
||||
if (result == DISP_CHANGE_SUCCESSFUL)
|
||||
{
|
||||
Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Failed to change display resolution. Error code: {result}");
|
||||
}
|
||||
}
|
||||
else if (result == DISP_CHANGE_RESTART)
|
||||
{
|
||||
Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight} requires a restart");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Failed to change display resolution. Error code: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 limitation could be removed either Auto-generate baseline image for both Light & Dark mode
|
||||
/// </summary>
|
||||
/// <param name="testContext">TestContext object</param>
|
||||
/// <param name="element">Element object</param>
|
||||
/// <param name="scenarioSubname">additional scenario name if two or more scenarios in one test</param>
|
||||
[RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")]
|
||||
public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "")
|
||||
{
|
||||
var pipelinePlatform = Environment.GetEnvironmentVariable("platform");
|
||||
|
||||
// Perform visual validation only in the pipeline
|
||||
if (string.IsNullOrEmpty(pipelinePlatform))
|
||||
{
|
||||
Console.WriteLine("Skip visual validation in the local run.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (element == null)
|
||||
{
|
||||
Assert.Fail("Element object is null or invalid");
|
||||
}
|
||||
|
||||
var stackTrace = new StackTrace();
|
||||
var callerFrame = stackTrace.GetFrame(1);
|
||||
var callerMethod = callerFrame?.GetMethod();
|
||||
|
||||
var callerName = callerMethod?.Name;
|
||||
var callerClassName = callerMethod?.DeclaringType?.Name;
|
||||
|
||||
if (string.IsNullOrEmpty(callerName) || string.IsNullOrEmpty(callerClassName))
|
||||
{
|
||||
Assert.Fail("Unable to determine the caller method and class name.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scenarioSubname))
|
||||
{
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, pipelinePlatform);
|
||||
}
|
||||
else
|
||||
{
|
||||
scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), pipelinePlatform);
|
||||
}
|
||||
|
||||
var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault();
|
||||
|
||||
var tempTestImagePath = GetTempFilePath(scenarioSubname, "test", ".png");
|
||||
|
||||
element.SaveToPngFile(tempTestImagePath);
|
||||
|
||||
if (string.IsNullOrEmpty(baselineImageResourceName)
|
||||
|| !Path.GetFileNameWithoutExtension(baselineImageResourceName).EndsWith(scenarioSubname))
|
||||
{
|
||||
testContext?.AddResultFile(tempTestImagePath);
|
||||
Assert.Fail($"Baseline image for scenario {scenarioSubname} can't be found, test image saved in file://{tempTestImagePath.Replace('\\', '/')}");
|
||||
}
|
||||
|
||||
var tempBaselineImagePath = GetTempFilePath(scenarioSubname, "baseline", Path.GetExtension(baselineImageResourceName));
|
||||
|
||||
bool isSame = false;
|
||||
|
||||
using (var stream = callerMethod!.DeclaringType!.Assembly.GetManifestResourceStream(baselineImageResourceName))
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
Assert.Fail($"Resource stream '{baselineImageResourceName}' is null.");
|
||||
}
|
||||
|
||||
using (var baselineImage = new Bitmap(stream))
|
||||
{
|
||||
using (var testImage = new Bitmap(tempTestImagePath))
|
||||
{
|
||||
isSame = VisualAssert.AreEqual(baselineImage, testImage);
|
||||
|
||||
if (!isSame)
|
||||
{
|
||||
// Copy baseline image to temp folder as well
|
||||
baselineImage.Save(tempBaselineImagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSame)
|
||||
{
|
||||
if (testContext != null)
|
||||
{
|
||||
testContext.AddResultFile(tempBaselineImagePath);
|
||||
testContext.AddResultFile(tempTestImagePath);
|
||||
}
|
||||
|
||||
Assert.Fail($"Fail to validate visual result for scenario {scenarioSubname}, baseline image can be found file://{tempBaselineImagePath.Replace('\\', '/')}, and test image can be found file://{tempTestImagePath.Replace('\\', '/')}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get temp file path
|
||||
/// </summary>
|
||||
/// <param name="scenario">scenario name</param>
|
||||
/// <param name="imageType">baseline or test image</param>
|
||||
/// <param name="extension">image file extension</param>
|
||||
/// <returns>full temp file path</returns>
|
||||
private static string GetTempFilePath(string scenario, string imageType, string extension)
|
||||
{
|
||||
var tempFileFullName = $"{scenario}_{imageType}{extension}";
|
||||
|
||||
// Remove invalid filename character if any
|
||||
Path.GetInvalidFileNameChars().ToList().ForEach(c => tempFileFullName = tempFileFullName.Replace(c, '-'));
|
||||
|
||||
return Path.Combine(Path.GetTempPath(), tempFileFullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if two images are equal bit-by-bit
|
||||
/// </summary>
|
||||
/// <param name="baselineImage">baseline image</param>
|
||||
/// <param name="testImage">test image</param>
|
||||
/// <returns>true if are equal,otherwise false</returns>
|
||||
private static bool AreEqual(Bitmap baselineImage, Bitmap testImage)
|
||||
{
|
||||
if (baselineImage.Width != testImage.Width || baselineImage.Height != testImage.Height)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// WinAppDriver sometimes adds a border to the screenshot (around 2 pix width), and it is not always consistent.
|
||||
// So we exclude the border when comparing the images, and usually it is the edge of the windows, won't affect the comparison.
|
||||
int excludeBorderWidth = 5, excludeBorderHeight = 5;
|
||||
|
||||
for (int x = excludeBorderWidth; x < baselineImage.Width - excludeBorderWidth; x++)
|
||||
{
|
||||
for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++)
|
||||
{
|
||||
if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/common/UITestAutomation/VisualHelper.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
internal static class VisualHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Compare two pixels with a fuzz factor
|
||||
/// </summary>
|
||||
/// <param name="c1">base color</param>
|
||||
/// <param name="c2">test color</param>
|
||||
/// <param name="fuzz">fuzz factor, default is 10</param>
|
||||
/// <returns>true if same, otherwise is false</returns>
|
||||
public static bool PixIsSame(Color c1, Color c2, int fuzz = 10)
|
||||
{
|
||||
return Math.Abs(c1.A - c2.A) <= fuzz && Math.Abs(c1.R - c2.R) <= fuzz && Math.Abs(c1.G - c2.G) <= fuzz && Math.Abs(c1.B - c2.B) <= fuzz;
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src/common/UITestAutomation/WindowHelper.cs
Normal file
@@ -0,0 +1,331 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
internal static class WindowHelper
|
||||
{
|
||||
internal const string AdministratorPrefix = "Administrator: ";
|
||||
|
||||
/// <summary>
|
||||
/// Sets the main window size.
|
||||
/// </summary>
|
||||
/// <param name="size">WindowSize enum</param>
|
||||
public static void SetWindowSize(IntPtr windowHandler, WindowSize size)
|
||||
{
|
||||
if (size == WindowSize.UnSpecified)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0, height = 0;
|
||||
|
||||
switch (size)
|
||||
{
|
||||
case WindowSize.Small:
|
||||
width = 640;
|
||||
height = 480;
|
||||
break;
|
||||
case WindowSize.Small_Vertical:
|
||||
width = 480;
|
||||
height = 640;
|
||||
break;
|
||||
case WindowSize.Medium:
|
||||
width = 1024;
|
||||
height = 768;
|
||||
break;
|
||||
case WindowSize.Medium_Vertical:
|
||||
width = 768;
|
||||
height = 1024;
|
||||
break;
|
||||
case WindowSize.Large:
|
||||
width = 1920;
|
||||
height = 1080;
|
||||
break;
|
||||
case WindowSize.Large_Vertical:
|
||||
width = 1080;
|
||||
height = 1920;
|
||||
break;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
WindowHelper.SetMainWindowSize(windowHandler, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(x, y)</returns>
|
||||
public static (int CenterX, int CenterY) GetWindowCenter(IntPtr windowHandler)
|
||||
{
|
||||
if (windowHandler == IntPtr.Zero)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rect = ApiHelper.GetWindowCenter(windowHandler);
|
||||
return (rect.CenterX, rect.CenterY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(x, y)</returns>
|
||||
public static (int Left, int Top, int Right, int Bottom) GetWindowRect(IntPtr windowHandler)
|
||||
{
|
||||
if (windowHandler == IntPtr.Zero)
|
||||
{
|
||||
return (0, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rect = ApiHelper.GetWindowRect(windowHandler);
|
||||
return (rect.Left, rect.Top, rect.Right, rect.Bottom);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the screen center coordinates.
|
||||
/// </summary>
|
||||
/// <returns>(x, y)</returns>
|
||||
public static (int CenterX, int CenterY) GetScreenCenter()
|
||||
{
|
||||
return ApiHelper.GetScreenCenter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the main window size based on Width and Height.
|
||||
/// </summary>
|
||||
/// <param name="width">the width in pixel</param>
|
||||
/// <param name="height">the height in pixel</param>
|
||||
public static void SetMainWindowSize(IntPtr windowHandler, int width, int height)
|
||||
{
|
||||
if (windowHandler == IntPtr.Zero
|
||||
|| width <= 0
|
||||
|| height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApiHelper.SetWindowPos(windowHandler, IntPtr.Zero, 0, 0, width, height, ApiHelper.SetWindowPosNoZorder | ApiHelper.SetWindowPosShowWindow);
|
||||
|
||||
// Wait for 1000ms after resize
|
||||
Task.Delay(1000).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the color of the pixel at the specified screen coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x">The X coordinate on the screen.</param>
|
||||
/// <param name="y">The Y coordinate on the screen.</param>
|
||||
/// <returns>The color of the pixel at the specified coordinates.</returns>
|
||||
public static Color GetPixelColor(int x, int y)
|
||||
{
|
||||
IntPtr hdc = ApiHelper.GetDC(IntPtr.Zero);
|
||||
uint pixel = ApiHelper.GetPixel(hdc, x, y);
|
||||
_ = ApiHelper.ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
int r = (int)(pixel & 0x000000FF);
|
||||
int g = (int)((pixel & 0x0000FF00) >> 8);
|
||||
int b = (int)((pixel & 0x00FF0000) >> 16);
|
||||
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the color of the pixel at the specified screen coordinates as a string.
|
||||
/// </summary>
|
||||
/// <param name="x">The X coordinate on the screen.</param>
|
||||
/// <param name="y">The Y coordinate on the screen.</param>
|
||||
/// <returns>The color of the pixel at the specified coordinates.</returns>
|
||||
public static string GetPixelColorString(int x, int y)
|
||||
{
|
||||
Color color = WindowHelper.GetPixelColor(x, y);
|
||||
return $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the display.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A tuple containing the width and height of the display.
|
||||
/// </returns
|
||||
public static Tuple<int, int> GetDisplaySize()
|
||||
{
|
||||
IntPtr hdc = ApiHelper.GetDC(IntPtr.Zero);
|
||||
int screenWidth = ApiHelper.GetDeviceCaps(hdc, ApiHelper.DESKTOPHORZRES);
|
||||
int screenHeight = ApiHelper.GetDeviceCaps(hdc, ApiHelper.DESKTOPVERTRES);
|
||||
_ = ApiHelper.ReleaseDC(IntPtr.Zero, hdc);
|
||||
|
||||
return Tuple.Create(screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
public static bool IsWindowOpen(string windowName)
|
||||
{
|
||||
var matchingWindows = ApiHelper.FindDesktopWindowHandler([windowName, AdministratorPrefix + windowName]);
|
||||
return matchingWindows.Count > 0;
|
||||
}
|
||||
|
||||
internal static class ApiHelper
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
public const uint SetWindowPosNoMove = 0x0002;
|
||||
public const uint SetWindowPosNoZorder = 0x0004;
|
||||
public const uint SetWindowPosShowWindow = 0x0040;
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
|
||||
// Delegate for the EnumWindows callback function
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
// P/Invoke declaration for EnumWindows
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
// P/Invoke declaration for GetWindowTextLength
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private static extern int GetWindowTextLength(IntPtr hWnd);
|
||||
|
||||
// P/Invoke declaration for GetWindowText
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
public static extern uint GetPixel(IntPtr hdc, int x, int y);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
public const int DESKTOPHORZRES = 118;
|
||||
public const int DESKTOPVERTRES = 117;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
// Define the Win32 RECT structure
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int Left; // X coordinate of the left edge of the window
|
||||
public int Top; // Y coordinate of the top edge of the window
|
||||
public int Right; // X coordinate of the right edge of the window
|
||||
public int Bottom; // Y coordinate of the bottom edge of the window
|
||||
}
|
||||
|
||||
// Import GetWindowRect API to retrieve window's screen coordinates
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
public static List<(IntPtr HWnd, string Title)> FindDesktopWindowHandler(string[] matchingWindowsTitles)
|
||||
{
|
||||
var windows = new List<(IntPtr HWnd, string Title)>();
|
||||
|
||||
_ = EnumWindows(
|
||||
(hWnd, lParam) =>
|
||||
{
|
||||
int length = GetWindowTextLength(hWnd);
|
||||
if (length > 0)
|
||||
{
|
||||
var builder = new StringBuilder(length + 1);
|
||||
_ = GetWindowText(hWnd, builder, builder.Capacity);
|
||||
|
||||
var title = builder.ToString();
|
||||
if (matchingWindowsTitles.Contains(title))
|
||||
{
|
||||
windows.Add((hWnd, title));
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Continue enumeration
|
||||
},
|
||||
IntPtr.Zero);
|
||||
|
||||
return windows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the center point coordinates of a specified window (in screen coordinates)
|
||||
/// </summary>
|
||||
/// <param name="hWnd">The window handle</param>
|
||||
/// <returns>The center point (x, y)</returns>
|
||||
public static (int CenterX, int CenterY) GetWindowCenter(IntPtr hWnd)
|
||||
{
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentException("Invalid window handle");
|
||||
}
|
||||
|
||||
if (GetWindowRect(hWnd, out RECT rect))
|
||||
{
|
||||
int width = rect.Right - rect.Left;
|
||||
int height = rect.Bottom - rect.Top;
|
||||
|
||||
int centerX = rect.Left + (width / 2);
|
||||
int centerY = rect.Top + (height / 2);
|
||||
|
||||
return (centerX, centerY);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Failed to retrieve window coordinates");
|
||||
}
|
||||
}
|
||||
|
||||
public static (int Left, int Top, int Right, int Bottom) GetWindowRect(IntPtr hWnd)
|
||||
{
|
||||
if (hWnd == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentException("Invalid window handle");
|
||||
}
|
||||
|
||||
if (GetWindowRect(hWnd, out RECT rect))
|
||||
{
|
||||
return (rect.Left, rect.Top, rect.Right, rect.Bottom);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Failed to retrieve window coordinates");
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int GetSystemMetrics(int nIndex);
|
||||
|
||||
public enum SystemMetric
|
||||
{
|
||||
ScreenWidth = 0, // Width of the primary screen in pixels (SM_CXSCREEN)
|
||||
ScreenHeight = 1, // Height of the primary screen in pixels (SM_CYSCREEN)
|
||||
VirtualScreenWidth = 78, // Width of the virtual screen that includes all monitors (SM_CXVIRTUALSCREEN)
|
||||
VirtualScreenHeight = 79, // Height of the virtual screen that includes all monitors (SM_CYVIRTUALSCREEN)
|
||||
MonitorCount = 80, // Number of display monitors (SM_CMONITORS, available on Windows XP+)
|
||||
}
|
||||
|
||||
public static (int CenterX, int CenterY) GetScreenCenter()
|
||||
{
|
||||
int width = GetSystemMetrics((int)SystemMetric.ScreenWidth);
|
||||
int height = GetSystemMetrics((int)SystemMetric.ScreenHeight);
|
||||
|
||||
return (width / 2, height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,6 +152,7 @@
|
||||
</data>
|
||||
<data name="ClipboardHistoryImage" xml:space="preserve">
|
||||
<value>Image data</value>
|
||||
<comment>Label used to represent an image in the clipboard history</comment>
|
||||
</data>
|
||||
<data name="ClipboardHistoryItemMoreOptionsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More options</value>
|
||||
@@ -194,15 +195,19 @@
|
||||
</data>
|
||||
<data name="TranscodeToMp3" xml:space="preserve">
|
||||
<value>Transcode to .mp3</value>
|
||||
<comment>Option to transcode audio files to MP3 format</comment>
|
||||
</data>
|
||||
<data name="TranscodeToMp4" xml:space="preserve">
|
||||
<value>Transcode to .mp4 (H.264/AAC)</value>
|
||||
<comment>Option to transcode video files to MP4 format with H.264 video codec and AAC audio codec</comment>
|
||||
</data>
|
||||
<data name="TranscodeErrorGeneral" xml:space="preserve">
|
||||
<value>An error occurred while transcoding media file</value>
|
||||
<comment>Error message displayed when media conversion fails for an unknown or general reason</comment>
|
||||
</data>
|
||||
<data name="TranscodeErrorUnsupportedCodec" xml:space="preserve">
|
||||
<value>The media file contains an unsupported codec</value>
|
||||
<comment>Error message displayed when media conversion fails due to an unsupported codec in the source file</comment>
|
||||
</data>
|
||||
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Paste</value>
|
||||
|
||||
|
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.ErrorMessageShowupIfNotRunAsAdmin")]
|
||||
[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.NoErrorMessageShowupIfRunAsAdmin")]
|
||||
[TestCategory("Hosts File Editor #8")]
|
||||
public void TestNoErrorMessageWithNonAdminPermission()
|
||||
{
|
||||
if (this.Session.IsElevated == true)
|
||||
{
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
|
||||
// Add new URL override and a warning tip should be shown
|
||||
this.AddEntry("192.168.0.1", "localhost", true);
|
||||
|
||||
Assert.IsFalse(
|
||||
this.FindAll<TextBlock>("The hosts file cannot be saved because the program isn't running as administrator.").Count > 0,
|
||||
"Should display host-file saving error if not run as administrator");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
@@ -243,25 +283,25 @@ namespace Hosts.UITests
|
||||
private void CloseWarningDialog()
|
||||
{
|
||||
// Find 'Accept' button which come in 'Warning' dialog
|
||||
if (this.FindAll("Warning").Count > 0 &&
|
||||
this.FindAll<Button>("Accept").Count > 0)
|
||||
if (this.FindAll("Warning", 1000).Count > 0 &&
|
||||
this.FindAll<Button>("Accept", 1000).Count > 0)
|
||||
{
|
||||
// Hide Warning dialog if any
|
||||
this.Find<Button>("Accept").Click();
|
||||
this.Find<Button>("Accept", 1000).Click();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAllEntries()
|
||||
{
|
||||
// Delete all existing host-override rules
|
||||
foreach (var deleteBtn in this.FindAll<Button>("Delete"))
|
||||
foreach (var deleteBtn in this.FindAll<Button>("Delete", 1000))
|
||||
{
|
||||
deleteBtn.Click();
|
||||
this.Find<Button>("Yes").Click();
|
||||
this.Find<Button>("Yes", 1000).Click();
|
||||
}
|
||||
|
||||
// Should have no row left, and no more delete button
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0);
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete", 1000).Count == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -51,10 +58,7 @@ namespace Hosts.UITests
|
||||
|
||||
this.Find<Button>("Launch Hosts File Editor").Click();
|
||||
|
||||
// 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 +72,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 +86,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);
|
||||
@@ -90,14 +94,9 @@ namespace Hosts.UITests
|
||||
|
||||
private bool IsHostsFileEditorClosed()
|
||||
{
|
||||
try
|
||||
if (this.Session.FindAll<Window>("Hosts File Editor").Count == 0 && this.Session.FindAll<Window>("Administrator: Hosts File Editor").Count == 0)
|
||||
{
|
||||
this.Session.FindAll<Window>("Hosts File Editor");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Validate if editor window closed by checking exception.Message
|
||||
return ex.Message.Contains("Currently selected window has been closed");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -121,7 +120,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,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\WinAppSdkCppPre.props" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\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">
|
||||
<CppWinRTOptimized>true</CppWinRTOptimized>
|
||||
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
|
||||
@@ -134,16 +136,24 @@
|
||||
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\WinAppSdkCppPost.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<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.7.250513003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
<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.7.250513003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250513003\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.7.250401001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.7.250513003" 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,498 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Windows.Devices.Printers;
|
||||
|
||||
namespace MouseUtils.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class MouseHighlighterTests : UITestBase
|
||||
{
|
||||
[TestMethod("MouseUtils.MouseHighlighter.EnableMouseHighlighter")]
|
||||
[TestCategory("Mouse Utils #17")]
|
||||
[TestCategory("Mouse Utils #18")]
|
||||
[TestCategory("Mouse Utils #19")]
|
||||
[TestCategory("Mouse Utils #20")]
|
||||
[TestCategory("Mouse Utils #21")]
|
||||
public void TestEnableMouseHighlighter()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Find My Mouse custom not found.");
|
||||
}
|
||||
|
||||
var settings = new MouseHighlighterSettings();
|
||||
settings.PrimaryButtonHighlightColor = "FFFF0000";
|
||||
settings.SecondaryButtonHighlightColor = "FF00FF00";
|
||||
settings.AlwaysHighlightColor = "004cFF71";
|
||||
settings.Radius = "50";
|
||||
settings.FadeDelay = "0";
|
||||
settings.FadeDuration = "90";
|
||||
|
||||
var foundCustom = this.Find<Custom>("Mouse Highlighter");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase]Change activation shortcut and test it
|
||||
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutButton);
|
||||
|
||||
activationShortcutButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutWindow);
|
||||
|
||||
// Invalid shortcut key
|
||||
Session.SendKeySequence(Key.H);
|
||||
|
||||
// IOUtil.SimulateKeyPress(0x41);
|
||||
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
|
||||
Assert.IsNotNull(invalidShortcutText);
|
||||
|
||||
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.H);
|
||||
|
||||
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
|
||||
var saveButton = activationShortcutWindow.Find<Button>("Save");
|
||||
Assert.IsNotNull(saveButton);
|
||||
saveButton.Click(false, 500, 1000);
|
||||
|
||||
SetMouseHighlighterAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var xy0 = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
|
||||
Session.PerformMouseAction(MouseActionType.LeftClick);
|
||||
|
||||
// Check the mouse highlighter is enabled
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.H);
|
||||
|
||||
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// MouseSimulator.LeftClick();
|
||||
// [Test Case] Press the activation shortcut and press left and right click somewhere, verifying the highlights are applied.
|
||||
// [Test Case] Press the activation shortcut again and verify no highlights appear when the mouse buttons are clicked.
|
||||
VerifyMouseHighlighterAppears(ref settings, "leftClick");
|
||||
VerifyMouseHighlighterAppears(ref settings, "rightClick");
|
||||
|
||||
// Disable mouse highlighter
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.H);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
VerifyMouseHighlighterNotAppears(ref settings, "leftClick");
|
||||
VerifyMouseHighlighterNotAppears(ref settings, "rightClick");
|
||||
|
||||
// [Test Case] Disable Mouse Highlighter and verify that the module is not activated when you press the activation shortcut.
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
|
||||
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.H);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
VerifyMouseHighlighterNotAppears(ref settings, "leftClick");
|
||||
VerifyMouseHighlighterNotAppears(ref settings, "rightClick");
|
||||
|
||||
// [Test Case] With left mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
|
||||
// [Test Case] With right mouse button pressed, drag the mouse and verify the hightlight is dragged with the pointer.
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1 - 100, xy.Item2);
|
||||
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.H);
|
||||
Task.Delay(1000).Wait();
|
||||
VerifyMouseHighlighterDrag(ref settings, "leftClick");
|
||||
VerifyMouseHighlighterDrag(ref settings, "rightClick");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Mouse Highlighter Custom not found.");
|
||||
}
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
[TestMethod("MouseUtils.MouseHighlighter.MouseHighlighterDifferentSettings")]
|
||||
[TestCategory("Mouse Utils #22")]
|
||||
[TestCategory("Mouse Utils #23")]
|
||||
[TestCategory("Mouse Utils #24")]
|
||||
public void TestMouseHighlighterDifferentSettings()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Find My Mouse custom not found.");
|
||||
}
|
||||
|
||||
var settings = new MouseHighlighterSettings();
|
||||
settings.PrimaryButtonHighlightColor = "FF000000";
|
||||
settings.SecondaryButtonHighlightColor = "FFFFFFFF";
|
||||
settings.AlwaysHighlightColor = "004cFF71";
|
||||
settings.Radius = "70";
|
||||
settings.FadeDelay = "0";
|
||||
settings.FadeDuration = "90";
|
||||
|
||||
var foundCustom = this.Find<Custom>("Mouse Highlighter");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(false);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Highlighter").Toggle(true);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase] Test the different settings and verify they apply - Change activation shortcut and test it
|
||||
// [Test Case] Test the different settings and verify they apply - Left button highlight color
|
||||
// [Test Case] Test the different settings and verify they apply - Right button highlight color
|
||||
// [Test Case] Test the different settings and verify they apply - Radius
|
||||
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutButton);
|
||||
|
||||
activationShortcutButton.Click();
|
||||
Task.Delay(500).Wait();
|
||||
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutWindow);
|
||||
|
||||
// Invalid shortcut key
|
||||
Session.SendKeySequence(Key.H);
|
||||
|
||||
// IOUtil.SimulateKeyPress(0x41);
|
||||
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
|
||||
Assert.IsNotNull(invalidShortcutText);
|
||||
|
||||
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.O);
|
||||
|
||||
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
|
||||
var saveButton = activationShortcutWindow.Find<Button>("Save");
|
||||
Assert.IsNotNull(saveButton);
|
||||
saveButton.Click(false, 500, 1000);
|
||||
|
||||
SetMouseHighlighterAppearanceBehavior(ref foundCustom, ref settings);
|
||||
|
||||
var xy0 = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy0.Item1 - 100, xy0.Item2);
|
||||
Session.PerformMouseAction(MouseActionType.LeftClick);
|
||||
|
||||
// Check the mouse highlighter is enabled
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.O);
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
VerifyMouseHighlighterAppears(ref settings, "leftClick");
|
||||
VerifyMouseHighlighterAppears(ref settings, "rightClick");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Mouse Highlighter Custom not found.");
|
||||
}
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
private void VerifyMouseHighlighterDrag(ref MouseHighlighterSettings settings, string action = "leftClick")
|
||||
{
|
||||
Task.Delay(500).Wait();
|
||||
string expectedColor = string.Empty;
|
||||
if (action == "leftClick")
|
||||
{
|
||||
IOUtil.SimulateMouseDown(true);
|
||||
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
IOUtil.SimulateMouseDown(false);
|
||||
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Invalid action specified.");
|
||||
}
|
||||
|
||||
expectedColor = "#" + expectedColor;
|
||||
Task.Delay(100).Wait();
|
||||
var location = Session.GetMousePosition();
|
||||
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
|
||||
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick);
|
||||
|
||||
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
|
||||
Assert.AreEqual(expectedColor, colorLeftClick2);
|
||||
|
||||
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
|
||||
Assert.AreNotEqual(expectedColor, colorBackground);
|
||||
|
||||
// Drag the mouse
|
||||
// Session.MoveMouseTo(location.Item1 - 400, location.Item2);
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
IOUtil.MoveMouseBy(-1, 0);
|
||||
Task.Delay(10).Wait();
|
||||
}
|
||||
|
||||
Task.Delay(2000).Wait();
|
||||
|
||||
location = Session.GetMousePosition();
|
||||
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick);
|
||||
|
||||
colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
|
||||
Assert.AreEqual(expectedColor, colorLeftClick2);
|
||||
|
||||
colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
|
||||
Assert.AreNotEqual(expectedColor, colorBackground);
|
||||
|
||||
if (action == "leftClick")
|
||||
{
|
||||
IOUtil.SimulateMouseUp(true);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
IOUtil.SimulateMouseUp(false);
|
||||
}
|
||||
|
||||
int duration = int.Parse(settings.FadeDuration, CultureInfo.InvariantCulture);
|
||||
Task.Delay(duration + 100).Wait();
|
||||
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreNotEqual("#" + settings.PrimaryButtonHighlightColor, colorLeftClick);
|
||||
}
|
||||
|
||||
private void VerifyMouseHighlighterNotAppears(ref MouseHighlighterSettings settings, string action = "leftClick")
|
||||
{
|
||||
Task.Delay(500).Wait();
|
||||
string expectedColor = string.Empty;
|
||||
if (action == "leftClick")
|
||||
{
|
||||
// MouseSimulator.LeftDown();
|
||||
Session.PerformMouseAction(MouseActionType.LeftDown);
|
||||
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
// MouseSimulator.RightDown();
|
||||
Session.PerformMouseAction(MouseActionType.RightDown);
|
||||
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Invalid action specified.");
|
||||
}
|
||||
|
||||
expectedColor = "#" + expectedColor;
|
||||
var location = Session.GetMousePosition();
|
||||
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
|
||||
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreNotEqual(expectedColor, colorLeftClick);
|
||||
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
Assert.AreNotEqual(expectedColor, colorLeftClick2);
|
||||
if (action == "leftClick")
|
||||
{
|
||||
Session.PerformMouseAction(MouseActionType.LeftUp);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
Session.PerformMouseAction(MouseActionType.RightUp);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyMouseHighlighterAppears(ref MouseHighlighterSettings settings, string action = "leftClick")
|
||||
{
|
||||
Task.Delay(500).Wait();
|
||||
string expectedColor = string.Empty;
|
||||
if (action == "leftClick")
|
||||
{
|
||||
// MouseSimulator.LeftDown();
|
||||
Session.PerformMouseAction(MouseActionType.LeftDown);
|
||||
expectedColor = settings.PrimaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
// MouseSimulator.RightDown();
|
||||
Session.PerformMouseAction(MouseActionType.RightDown);
|
||||
expectedColor = settings.SecondaryButtonHighlightColor.Substring(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Invalid action specified.");
|
||||
}
|
||||
|
||||
expectedColor = "#" + expectedColor;
|
||||
|
||||
var location = Session.GetMousePosition();
|
||||
int radius = int.Parse(settings.Radius, CultureInfo.InvariantCulture);
|
||||
var colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreEqual(expectedColor, colorLeftClick);
|
||||
|
||||
var colorLeftClick2 = this.GetPixelColorString(location.Item1 + radius - 1, location.Item2);
|
||||
|
||||
Assert.AreEqual(expectedColor, colorLeftClick2);
|
||||
Task.Delay(500).Wait();
|
||||
|
||||
var colorBackground = this.GetPixelColorString(location.Item1 + radius + 50, location.Item2 + radius + 50);
|
||||
Assert.AreNotEqual(expectedColor, colorBackground);
|
||||
if (action == "leftClick")
|
||||
{
|
||||
// MouseSimulator.LeftUp();
|
||||
Session.PerformMouseAction(MouseActionType.LeftUp);
|
||||
}
|
||||
else if (action == "rightClick")
|
||||
{
|
||||
// MouseSimulator.RightUp();
|
||||
Session.PerformMouseAction(MouseActionType.RightUp);
|
||||
}
|
||||
|
||||
int duration = int.Parse(settings.FadeDuration, CultureInfo.InvariantCulture);
|
||||
Task.Delay(duration + 100).Wait();
|
||||
colorLeftClick = this.GetPixelColorString(location.Item1, location.Item2);
|
||||
Assert.AreNotEqual("#" + settings.PrimaryButtonHighlightColor, colorLeftClick);
|
||||
}
|
||||
|
||||
private void SetColor(ref Custom foundCustom, string colorName = "Primary button highlight color", string colorValue = "000000", string opacity = "0")
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
if (foundCustom.FindAll<TextBox>("Fade duration (ms) Minimum0").Count == 0)
|
||||
{
|
||||
groupAppearanceBehavior.Click();
|
||||
}
|
||||
|
||||
// Set primary button highlight color
|
||||
var primaryButtonHighlightColor = foundCustom.Find<Group>(colorName);
|
||||
Assert.IsNotNull(primaryButtonHighlightColor);
|
||||
|
||||
var button = primaryButtonHighlightColor.Find<Button>(By.XPath(".//Button"));
|
||||
Assert.IsNotNull(button);
|
||||
button.Click(false, 500, 700);
|
||||
|
||||
var popupWindow = Session.Find<Window>("Popup");
|
||||
Assert.IsNotNull(popupWindow);
|
||||
var colorModelComboBox = this.Find<ComboBox>("Color model");
|
||||
Assert.IsNotNull(colorModelComboBox);
|
||||
colorModelComboBox.Click(false, 500, 700);
|
||||
var selectedItem = colorModelComboBox.Find<NavigationViewItem>("RGB");
|
||||
selectedItem.Click();
|
||||
var rgbHexEdit = this.Find<TextBox>("RGB hex");
|
||||
Assert.IsNotNull(rgbHexEdit);
|
||||
Task.Delay(500).Wait();
|
||||
rgbHexEdit.SetText(colorValue);
|
||||
int retry = 5;
|
||||
while (retry > 0)
|
||||
{
|
||||
Task.Delay(500).Wait();
|
||||
rgbHexEdit.SetText(colorValue);
|
||||
Task.Delay(500).Wait();
|
||||
string rgbHex = rgbHexEdit.Text;
|
||||
bool isValid = rgbHex.StartsWith('#') && rgbHex.Length == 9 && rgbHex.Substring(1) == colorValue;
|
||||
Task.Delay(500).Wait();
|
||||
if (isValid)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
retry--;
|
||||
}
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
button.Click();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetMouseHighlighterAppearanceBehavior(ref Custom foundCustom, ref MouseHighlighterSettings settings)
|
||||
{
|
||||
Assert.IsNotNull(foundCustom);
|
||||
var groupAppearanceBehavior = foundCustom.Find<TextBlock>("Appearance & behavior");
|
||||
if (groupAppearanceBehavior != null)
|
||||
{
|
||||
// groupAppearanceBehavior.Click();
|
||||
if (foundCustom.FindAll<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDurationEdit)).Count == 0)
|
||||
{
|
||||
groupAppearanceBehavior.Click();
|
||||
}
|
||||
|
||||
// Set primary button highlight color
|
||||
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.PrimaryButtonHighlightColorGroup), settings.PrimaryButtonHighlightColor);
|
||||
|
||||
// Set secondary button highlight color
|
||||
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.SecondaryButtonHighlightColorGroup), settings.SecondaryButtonHighlightColor);
|
||||
|
||||
// Set the duration to duration ms
|
||||
var fadeDurationEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDurationEdit));
|
||||
Assert.IsNotNull(fadeDurationEdit);
|
||||
fadeDurationEdit.SetText(settings.FadeDuration);
|
||||
Assert.AreEqual(settings.FadeDuration, fadeDurationEdit.Text);
|
||||
|
||||
// Set Fade delay(ms)
|
||||
var fadeDelayEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.FadeDelayEdit));
|
||||
Assert.IsNotNull(fadeDelayEdit);
|
||||
fadeDelayEdit.SetText(settings.FadeDelay);
|
||||
Assert.AreEqual(settings.FadeDelay, fadeDelayEdit.Text);
|
||||
|
||||
// Set the fade radius (px)
|
||||
var fadeRadiusEdit = foundCustom.Find<TextBox>(settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.RadiusEdit));
|
||||
Assert.IsNotNull(fadeRadiusEdit);
|
||||
fadeRadiusEdit.SetText(settings.Radius);
|
||||
Assert.AreEqual(settings.Radius, fadeRadiusEdit.Text);
|
||||
|
||||
// Set always highlight color
|
||||
SetColor(ref foundCustom, settings.GetElementName(MouseHighlighterSettings.SettingsUIElements.AlwaysHighlightColorGroup), settings.AlwaysHighlightColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Appearance & behavior group not found.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchFromSetting(bool showWarning = false, bool launchAsAdmin = false)
|
||||
{
|
||||
this.Session.SetMainWindowSize(WindowSize.Large);
|
||||
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").Click();
|
||||
}
|
||||
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
250
src/modules/MouseUtils/MouseUtils.UITests/MouseJumpTests.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace MouseUtils.UITests
|
||||
{
|
||||
[TestClass]
|
||||
public class MouseJumpTests : UITestBase
|
||||
{
|
||||
[TestMethod("MouseUtils.MouseJump.EnableMouseJump")]
|
||||
[TestCategory("Mouse Utils #39")]
|
||||
[TestCategory("Mouse Utils #40")]
|
||||
[TestCategory("Mouse Utils #41")]
|
||||
[TestCategory("Mouse Utils #45")]
|
||||
public void TestEnableMouseJump()
|
||||
{
|
||||
LaunchFromSetting(true);
|
||||
}
|
||||
|
||||
[TestMethod("MouseUtils.MouseJump.EnableMouseJump2")]
|
||||
[TestCategory("Mouse Utils #39")]
|
||||
[TestCategory("Mouse Utils #41")]
|
||||
[TestCategory("Mouse Utils #45")]
|
||||
public void TestEnableMouseJump2()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Find My Mouse custom not found.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
}
|
||||
|
||||
var foundCustom = this.Find<Custom>("Mouse Jump");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase]Change activation shortcut and test it
|
||||
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutButton);
|
||||
|
||||
activationShortcutButton.Click(false, 500, 1000);
|
||||
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutWindow);
|
||||
|
||||
// Invalid shortcut key
|
||||
Session.SendKeySequence(Key.H);
|
||||
|
||||
// IOUtil.SimulateKeyPress(0x41);
|
||||
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
|
||||
Assert.IsNotNull(invalidShortcutText);
|
||||
|
||||
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
|
||||
|
||||
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
|
||||
var saveButton = activationShortcutWindow.Find<Button>("Save");
|
||||
Assert.IsNotNull(saveButton);
|
||||
saveButton.Click(false, 500, 1500);
|
||||
|
||||
var screenCenter = this.GetScreenCenter();
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY, 500, 1000);
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
|
||||
|
||||
// [TestCase] Enable Mouse Jump. Then - Press the activation shortcut and verify the screens preview appears.
|
||||
// [TestCase] Enable Mouse Jump. Then - Click around the screen preview and ensure that mouse cursor jumped to clicked location.
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
|
||||
VerifyWindowAppears();
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// [TestCase] Enable Mouse Jump. Then - Disable Mouse Jump and verify that the module is not activated when you press the activation shortcut.
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(false);
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.Z);
|
||||
Task.Delay(500).Wait();
|
||||
VerifyWindowNotAppears();
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Mouse Highlighter Custom not found.");
|
||||
}
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
[TestMethod("MouseUtils.MouseJump.EnableMouseJump3")]
|
||||
[TestCategory("Mouse Utils #40")]
|
||||
public void TestEnableMouseJump3()
|
||||
{
|
||||
LaunchFromSetting();
|
||||
var foundCustom0 = this.Find<Custom>("Find My Mouse");
|
||||
if (foundCustom0 != null)
|
||||
{
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(true);
|
||||
foundCustom0.Find<ToggleSwitch>("Enable Find My Mouse").Toggle(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Find My Mouse custom not found.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Session.PerformMouseAction(MouseActionType.ScrollDown);
|
||||
}
|
||||
|
||||
var foundCustom = this.Find<Custom>("Mouse Jump");
|
||||
if (foundCustom != null)
|
||||
{
|
||||
foundCustom.Find<ToggleSwitch>("Enable Mouse Jump").Toggle(true);
|
||||
|
||||
var xy = Session.GetMousePosition();
|
||||
Session.MoveMouseTo(xy.Item1, xy.Item2 - 100);
|
||||
|
||||
// Change the shortcut key for MouseHighlighter
|
||||
// [TestCase]Change activation shortcut and test it
|
||||
var activationShortcutButton = foundCustom.Find<Button>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutButton);
|
||||
|
||||
activationShortcutButton.Click(false, 500, 1000);
|
||||
var activationShortcutWindow = Session.Find<Window>("Activation shortcut");
|
||||
Assert.IsNotNull(activationShortcutWindow);
|
||||
|
||||
// Invalid shortcut key
|
||||
Session.SendKeySequence(Key.H);
|
||||
|
||||
// IOUtil.SimulateKeyPress(0x41);
|
||||
var invalidShortcutText = activationShortcutWindow.Find<TextBlock>("Invalid shortcut");
|
||||
Assert.IsNotNull(invalidShortcutText);
|
||||
|
||||
// IOUtil.SimulateShortcut(0x5B, 0x10, 0x45);
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.J);
|
||||
|
||||
// Assert.IsNull(activationShortcutWindow.Find<TextBlock>("Invalid shortcut"));
|
||||
var saveButton = activationShortcutWindow.Find<Button>("Save");
|
||||
Assert.IsNotNull(saveButton);
|
||||
saveButton.Click(false, 500, 1500);
|
||||
|
||||
var screenCenter = this.GetScreenCenter();
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY, 500, 1000);
|
||||
Session.MoveMouseTo(screenCenter.CenterX, screenCenter.CenterY - 300, 500, 1000);
|
||||
|
||||
// [TestCase] Enable Mouse Jump. Then - Change activation shortcut and verify that new shortcut triggers Mouse Jump.
|
||||
Session.SendKeys(Key.Win, Key.Shift, Key.J);
|
||||
VerifyWindowAppears();
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Fail("Mouse Highlighter Custom not found.");
|
||||
}
|
||||
|
||||
Task.Delay(500).Wait();
|
||||
}
|
||||
|
||||
private void VerifyWindowAppears()
|
||||
{
|
||||
string windowName = "MouseJump";
|
||||
Session.Attach(windowName);
|
||||
var center = this.Session.GetMainWindowCenter();
|
||||
Session.MoveMouseTo(center.CenterX, center.CenterY);
|
||||
Session.PerformMouseAction(MouseActionType.LeftClick, 1000, 1000);
|
||||
var screenCenter = this.GetScreenCenter();
|
||||
|
||||
// Get Mouse position
|
||||
var xy = Session.GetMousePosition();
|
||||
|
||||
double distance = CalculateDistance(xy.Item1, xy.Item2, screenCenter.CenterX, screenCenter.CenterY);
|
||||
Assert.IsTrue(distance <= 10, "Mouse Jump window should be opened and mouse should be moved to the center of the screen.");
|
||||
}
|
||||
|
||||
private void VerifyWindowNotAppears()
|
||||
{
|
||||
string windowName = "MouseJump";
|
||||
bool open = this.IsWindowOpen(windowName);
|
||||
Assert.IsFalse(open, "Mouse Jump window should not be opened.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the Euclidean distance between two 2D points
|
||||
/// </summary>
|
||||
/// <param name="x1">X coordinate of first point</param>
|
||||
/// <param name="y1">Y coordinate of first point</param>
|
||||
/// <param name="x2">X coordinate of second point</param>
|
||||
/// <param name="y2">Y coordinate of second point</param>
|
||||
/// <returns>Distance (double)</returns>
|
||||
public double CalculateDistance(int x1, int y1, int x2, int y2)
|
||||
{
|
||||
int dx = x2 - x1;
|
||||
int dy = y2 - y1;
|
||||
return Math.Sqrt((dx * dx) + (dy * dy));
|
||||
}
|
||||
|
||||
private void LaunchFromSetting(bool firstTime = false, bool launchAsAdmin = false)
|
||||
{
|
||||
Session.SetMainWindowSize(WindowSize.Large);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
|
||||
// Goto Hosts File Editor setting page
|
||||
if (this.FindAll<NavigationViewItem>("Mouse utilities").Count == 0)
|
||||
{
|
||||
RestartScopeExe();
|
||||
Session.SetMainWindowSize(WindowSize.Large);
|
||||
Task.Delay(1000).Wait();
|
||||
|
||||
// Expand Advanced list-group if needed
|
||||
this.Find<NavigationViewItem>("Input / Output").ClickCenter();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
|
||||
// Click on the Mouse utilities
|
||||
// Task.Delay(2000).Wait();
|
||||
if (firstTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Find<NavigationViewItem>("Mouse utilities").Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 shortcut triggers Mouse Jump.
|
||||
- [x] Click around the screen preview and ensure that mouse cursor jumped to clicked location.
|
||||
- [ ] Reorder screens in Display settings and confirm that Mouse Jump reflects the change and still works correctly.
|
||||
- [ ] Change scaling of screens and confirm that Mouse Jump still works correctly.
|
||||
- [ ] Unplug additional monitors and confirm that Mouse Jump still works correctly.
|
||||
- [x] Disable Mouse Jump and verify that the module is not activated when you press the activation shortcut.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/modules/MouseUtils/MouseUtils.UITests/util/IOUtil.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
// The MouseUtils module relies on simulating system-level input events (such as mouse movement or key presses) to test visual or behavioral responses.
|
||||
// The UI Test framework provides built-in methods for simulating mouse movement and clicks, which work for MouseUtils reliably on high-performance dev boxes.
|
||||
// However, on low-performance environments such as CI/CD pipelines or virtual machines, these simulated input events are not always correctly recognized by the operating system.
|
||||
// IOUtils class is added specifically for MouseUtils tests.
|
||||
// For any test scenario that involves simulating continuous mouse movement (e.g., detecting crosshair changes while moving the cursor),
|
||||
// input simulation methods in IOUtils class should be used.
|
||||
namespace MouseUtils.UITests
|
||||
{
|
||||
public class IOUtil
|
||||
{
|
||||
private readonly UIntPtr ignoreKeyEventFlag = 0x5555;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
|
||||
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
|
||||
internal struct INPUT
|
||||
{
|
||||
internal INPUTTYPE type;
|
||||
internal InputUnion data;
|
||||
|
||||
internal static int Size
|
||||
{
|
||||
get { return Marshal.SizeOf<INPUT>(); }
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct InputUnion
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
internal MOUSEINPUT mi;
|
||||
|
||||
[FieldOffset(0)]
|
||||
internal KEYBDINPUT ki;
|
||||
|
||||
[FieldOffset(0)]
|
||||
internal HARDWAREINPUT hi;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
|
||||
internal struct MOUSEINPUT
|
||||
{
|
||||
internal int dx;
|
||||
internal int dy;
|
||||
internal int mouseData;
|
||||
internal uint dwFlags;
|
||||
internal uint time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
|
||||
internal struct KEYBDINPUT
|
||||
{
|
||||
internal short wVk;
|
||||
internal short wScan;
|
||||
internal uint dwFlags;
|
||||
internal int time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct HARDWAREINPUT
|
||||
{
|
||||
internal int uMsg;
|
||||
internal short wParamL;
|
||||
internal short wParamH;
|
||||
}
|
||||
|
||||
internal enum INPUTTYPE : uint
|
||||
{
|
||||
INPUT_MOUSE = 0,
|
||||
INPUT_KEYBOARD = 1,
|
||||
INPUT_HARDWARE = 2,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum KeyEventF
|
||||
{
|
||||
KeyDown = 0x0000,
|
||||
ExtendedKey = 0x0001,
|
||||
KeyUp = 0x0002,
|
||||
Unicode = 0x0004,
|
||||
Scancode = 0x0008,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum MouseEventF : uint
|
||||
{
|
||||
MOVE = 0x0001,
|
||||
LEFTDOWN = 0x0002,
|
||||
LEFTUP = 0x0004,
|
||||
RIGHTDOWN = 0x0008,
|
||||
RIGHTUP = 0x0010,
|
||||
ABSOLUTE = 0x8000,
|
||||
MIDDLEDOWN = 0x0020,
|
||||
MIDDLEUP = 0x0040,
|
||||
}
|
||||
|
||||
public static void SimulateMouseDown(bool leftButton = true)
|
||||
{
|
||||
SendMouseInput(leftButton ? MouseEventF.LEFTDOWN : MouseEventF.RIGHTDOWN);
|
||||
}
|
||||
|
||||
public static void SimulateMouseUp(bool leftButton = true)
|
||||
{
|
||||
SendMouseInput(leftButton ? MouseEventF.LEFTUP : MouseEventF.RIGHTUP);
|
||||
}
|
||||
|
||||
public static void MouseClick(bool leftButton = true)
|
||||
{
|
||||
SendMouseInput(leftButton ? MouseEventF.LEFTDOWN : MouseEventF.RIGHTDOWN);
|
||||
SendMouseInput(leftButton ? MouseEventF.LEFTUP : MouseEventF.RIGHTUP);
|
||||
}
|
||||
|
||||
private static void SendMouseInput(MouseEventF mouseFlags)
|
||||
{
|
||||
var input = new INPUT
|
||||
{
|
||||
type = INPUTTYPE.INPUT_MOUSE,
|
||||
data = new InputUnion
|
||||
{
|
||||
mi = new MOUSEINPUT
|
||||
{
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
mouseData = 0,
|
||||
dwFlags = (uint)mouseFlags,
|
||||
time = 0,
|
||||
dwExtraInfo = UIntPtr.Zero,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
INPUT[] inputs = [input];
|
||||
_ = SendInput(1, inputs, INPUT.Size);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetMessageExtraInfo();
|
||||
|
||||
public static void MoveMouseBy(int dx, int dy)
|
||||
{
|
||||
var input = new INPUT
|
||||
{
|
||||
type = INPUTTYPE.INPUT_MOUSE,
|
||||
data = new InputUnion
|
||||
{
|
||||
mi = new MOUSEINPUT
|
||||
{
|
||||
dx = dx,
|
||||
dy = dy,
|
||||
mouseData = 0,
|
||||
dwFlags = (uint)MouseEventF.MOVE,
|
||||
time = 0,
|
||||
dwExtraInfo = (nuint)GetMessageExtraInfo(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
INPUT[] inputs = [input];
|
||||
_ = SendInput(1, inputs, INPUT.Size);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetSystemMetrics(int nIndex);
|
||||
|
||||
public static void MoveMouseTo(int x, int y)
|
||||
{
|
||||
int screenWidth = GetSystemMetrics(0);
|
||||
int screenHeight = GetSystemMetrics(1);
|
||||
|
||||
int normalizedX = (int)(x * 65535 / screenWidth);
|
||||
int normalizedY = (int)(y * 65535 / screenHeight);
|
||||
|
||||
var input = new INPUT
|
||||
{
|
||||
type = INPUTTYPE.INPUT_MOUSE,
|
||||
data = new InputUnion
|
||||
{
|
||||
mi = new MOUSEINPUT
|
||||
{
|
||||
dx = normalizedX,
|
||||
dy = normalizedY,
|
||||
mouseData = 0,
|
||||
dwFlags = (uint)(MouseEventF.MOVE | MouseEventF.ABSOLUTE),
|
||||
time = 0,
|
||||
dwExtraInfo = UIntPtr.Zero,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
INPUT[] inputs = [input];
|
||||
_ = SendInput(1, inputs, INPUT.Size);
|
||||
}
|
||||
|
||||
private void SendSingleKeyboardInput(short keyCode, uint keyStatus)
|
||||
{
|
||||
var inputShift = new INPUT
|
||||
{
|
||||
type = INPUTTYPE.INPUT_KEYBOARD,
|
||||
data = new InputUnion
|
||||
{
|
||||
ki = new KEYBDINPUT
|
||||
{
|
||||
wVk = keyCode,
|
||||
dwFlags = keyStatus,
|
||||
|
||||
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
|
||||
dwExtraInfo = ignoreKeyEventFlag,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
INPUT[] inputs = [inputShift];
|
||||
_ = SendInput(1, inputs, INPUT.Size);
|
||||
}
|
||||
|
||||
public static void SimulateKeyDown(ushort keyCode)
|
||||
{
|
||||
SendKey(keyCode, false);
|
||||
}
|
||||
|
||||
public static void SimulateKeyUp(ushort keyCode)
|
||||
{
|
||||
SendKey(keyCode, true);
|
||||
}
|
||||
|
||||
public static void SimulateKeyPress(ushort keyCode)
|
||||
{
|
||||
SendKey(keyCode, false);
|
||||
SendKey(keyCode, true);
|
||||
}
|
||||
|
||||
public static void SimulateShortcut(params ushort[] keyCodes)
|
||||
{
|
||||
foreach (var key in keyCodes)
|
||||
{
|
||||
SimulateKeyDown(key);
|
||||
}
|
||||
|
||||
for (int i = keyCodes.Length - 1; i >= 0; i--)
|
||||
{
|
||||
SimulateKeyUp(keyCodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendKey(ushort keyCode, bool keyUp)
|
||||
{
|
||||
var inputShift = new INPUT
|
||||
{
|
||||
type = INPUTTYPE.INPUT_KEYBOARD,
|
||||
data = new InputUnion
|
||||
{
|
||||
ki = new KEYBDINPUT
|
||||
{
|
||||
wVk = (short)keyCode,
|
||||
dwFlags = (uint)(keyUp ? KeyEventF.KeyUp : 0),
|
||||
dwExtraInfo = (uint)IntPtr.Zero,
|
||||
},
|
||||
},
|
||||
};
|
||||
INPUT[] inputs = [inputShift];
|
||||
|
||||
_ = SendInput(1, inputs, INPUT.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/processApi.h>
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
|
||||
HINSTANCE g_hInst_MouseWithoutBorders = 0;
|
||||
|
||||
@@ -409,9 +410,12 @@ public:
|
||||
{
|
||||
app_name = L"MouseWithoutBorders";
|
||||
app_key = app_name;
|
||||
std::filesystem::path logFilePath(PTSettingsHelper::get_module_save_folder_location(app_key));
|
||||
logFilePath.append(LogSettings::mouseWithoutBordersLogPath);
|
||||
Logger::init(LogSettings::mouseWithoutBordersLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
|
||||
|
||||
LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::mouseWithoutBordersLoggerName);
|
||||
|
||||
std::filesystem::path oldLogPath(PTSettingsHelper::get_module_save_folder_location(app_key));
|
||||
oldLogPath.append("LogsModuleInterface");
|
||||
LoggerHelpers::delete_old_log_folder(oldLogPath);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
@@ -9,7 +9,7 @@
|
||||
<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.7.250401001" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
<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.5" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>TemplateCmdPalExtension</RootNamespace>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="Styles/Button.xaml" />
|
||||
<ResourceDictionary Source="Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="Controls/Tag.xaml" />
|
||||
|
||||
@@ -34,8 +34,13 @@
|
||||
Orientation="Vertical"
|
||||
Spacing="4" />
|
||||
|
||||
<!-- Template for actions in the mode actions dropdown button -->
|
||||
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<cmdpalUI:ContextItemTemplateSelector
|
||||
x:Key="ContextItemTemplateSelector"
|
||||
Critical="{StaticResource CriticalContextMenuViewModelTemplate}"
|
||||
Default="{StaticResource DefaultContextMenuViewModelTemplate}" />
|
||||
|
||||
<!-- Template for context items in the context item menu -->
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
@@ -63,6 +68,38 @@
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for context items flagged as critical -->
|
||||
<DataTemplate x:Key="CriticalContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="4,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ContextItemTitleTextBlockCriticalStyle}"
|
||||
Text="{x:Bind Title, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="16,0,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ContextItemCaptionTextBlockCriticalStyle}"
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
@@ -237,7 +274,7 @@
|
||||
Margin="-16,-12,-16,-12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
|
||||
KeyDown="CommandsDropdown_KeyDown"
|
||||
SelectionMode="Single">
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.CmdPal.UI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate? Default { get; set; }
|
||||
|
||||
public DataTemplate? Critical { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item)
|
||||
{
|
||||
return ((CommandContextItemViewModel)item).IsCritical ? Critical : Default;
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
|
||||
<Target Name="RearrangeXefVersioningAndWinAppSDKResourceGeneration"
|
||||
DependsOnTargets="GetNewAppManifestValues;CreateWinRTRegistration"
|
||||
BeforeTargets="XesWriteVersionInfoResourceFile">
|
||||
<Target Name="RearrangeXefVersioningAndWinAppSDKResourceGeneration" DependsOnTargets="GetNewAppManifestValues;CreateWinRTRegistration" BeforeTargets="XesWriteVersionInfoResourceFile">
|
||||
<PropertyGroup>
|
||||
<!-- XES uses this property to store the "final" location of the app manifest, before it erases it.
|
||||
We have to update it to the value WinAppSDK set it to. -->
|
||||
|
||||
27
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TextBlock.xaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style
|
||||
x:Key="ContextItemTitleTextBlockCriticalStyle"
|
||||
BasedOn="{StaticResource BaseTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ContextItemCaptionTextBlockCriticalStyle"
|
||||
BasedOn="{StaticResource CaptionTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" 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')" />
|
||||
<PropertyGroup>
|
||||
<PathToRoot>..\..\..\..\</PathToRoot>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001</WasdkNuget>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250513003</WasdkNuget>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#  Command Palette
|
||||
|
||||
Windows Command Palette ("CmdPal") is the next iteration of PowerToys Run. With extensibility at it's core, the Command Palette is your one-stop launcher to start _anything_.
|
||||
Windows Command Palette ("CmdPal") is the next iteration of PowerToys Run. With extensibility at its core, the Command Palette is your one-stop launcher to start _anything_.
|
||||
|
||||
By default, CmdPal is bound to <kbd>Win+Alt+Space</kbd>.
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public static class CalculateHelper
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
throw new ArgumentNullException(paramName: nameof(input));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RegValidExpressChar.IsMatch(input))
|
||||
|
||||
@@ -23,6 +23,9 @@ public static partial class QueryHelper
|
||||
CultureInfo inputCulture = settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
|
||||
CultureInfo outputCulture = settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
|
||||
|
||||
// In case the user pastes a query with a leading =
|
||||
query = query.TrimStart('=');
|
||||
|
||||
// Happens if the user has only typed the action key so far
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
@@ -32,6 +35,11 @@ public static partial class QueryHelper
|
||||
NumberTranslator translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US"));
|
||||
var input = translator.Translate(query.Normalize(NormalizationForm.FormKC));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_expression_empty);
|
||||
}
|
||||
|
||||
if (!CalculateHelper.InputValid(input))
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -132,6 +132,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please enter an expression.
|
||||
/// </summary>
|
||||
public static string calculator_expression_empty {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_expression_empty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Expression wrong or incomplete.
|
||||
/// </summary>
|
||||
|
||||
@@ -199,4 +199,7 @@
|
||||
<data name="calculator_copy_binary" xml:space="preserve">
|
||||
<value>Copy binary</value>
|
||||
</data>
|
||||
<data name="calculator_expression_empty" xml:space="preserve">
|
||||
<value>Please enter an expression</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System.Helpers;
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")]
|
||||
public sealed class Native
|
||||
{
|
||||
public enum HRESULT : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// Operation successful.
|
||||
/// </summary>
|
||||
S_OK = 0x00000000,
|
||||
|
||||
/// <summary>
|
||||
/// Operation successful. (negative condition/no operation)
|
||||
/// </summary>
|
||||
S_FALSE = 0x00000001,
|
||||
|
||||
/// <summary>
|
||||
/// Not implemented.
|
||||
/// </summary>
|
||||
E_NOTIMPL = 0x80004001,
|
||||
|
||||
/// <summary>
|
||||
/// No such interface supported.
|
||||
/// </summary>
|
||||
E_NOINTERFACE = 0x80004002,
|
||||
|
||||
/// <summary>
|
||||
/// Pointer that is not valid.
|
||||
/// </summary>
|
||||
E_POINTER = 0x80004003,
|
||||
|
||||
/// <summary>
|
||||
/// Operation aborted.
|
||||
/// </summary>
|
||||
E_ABORT = 0x80004004,
|
||||
|
||||
/// <summary>
|
||||
/// Unspecified failure.
|
||||
/// </summary>
|
||||
E_FAIL = 0x80004005,
|
||||
|
||||
/// <summary>
|
||||
/// Unexpected failure.
|
||||
/// </summary>
|
||||
E_UNEXPECTED = 0x8000FFFF,
|
||||
|
||||
/// <summary>
|
||||
/// General access denied error.
|
||||
/// </summary>
|
||||
E_ACCESSDENIED = 0x80070005,
|
||||
|
||||
/// <summary>
|
||||
/// Handle that is not valid.
|
||||
/// </summary>
|
||||
E_HANDLE = 0x80070006,
|
||||
|
||||
/// <summary>
|
||||
/// Failed to allocate necessary memory.
|
||||
/// </summary>
|
||||
E_OUTOFMEMORY = 0x8007000E,
|
||||
|
||||
/// <summary>
|
||||
/// One or more arguments are not valid.
|
||||
/// </summary>
|
||||
E_INVALIDARG = 0x80070057,
|
||||
|
||||
/// <summary>
|
||||
/// The operation was canceled by the user. (Error source 7 means Win32.)
|
||||
/// </summary>
|
||||
/// <SeeAlso href="https://learn.microsoft.com/windows/win32/debug/system-error-codes--1000-1299-"/>
|
||||
/// <SeeAlso href="https://en.wikipedia.org/wiki/HRESULT"/>
|
||||
E_CANCELLED = 0x800704C7,
|
||||
}
|
||||
|
||||
public static class ShellItemTypeConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Guid for type IShellItem.
|
||||
/// </summary>
|
||||
public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe");
|
||||
|
||||
/// <summary>
|
||||
/// Guid for type IShellItem2.
|
||||
/// </summary>
|
||||
public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The following are ShellItem DisplayName types.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum SIGDN : uint
|
||||
{
|
||||
NORMALDISPLAY = 0,
|
||||
PARENTRELATIVEPARSING = 0x80018001,
|
||||
PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
|
||||
DESKTOPABSOLUTEPARSING = 0x80028000,
|
||||
PARENTRELATIVEEDITING = 0x80031001,
|
||||
DESKTOPABSOLUTEEDITING = 0x8004c000,
|
||||
FILESYSPATH = 0x80058000,
|
||||
URL = 0x80068000,
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
||||
public interface IShellItem
|
||||
{
|
||||
void BindToHandler(
|
||||
nint pbc,
|
||||
[MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
|
||||
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
|
||||
out nint ppv);
|
||||
|
||||
void GetParent(out IShellItem ppsi);
|
||||
|
||||
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
|
||||
|
||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
||||
|
||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see href="https://learn.microsoft.com/windows/win32/stg/stgm-constants">see all STGM values</see>
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum STGM : long
|
||||
{
|
||||
READ = 0x00000000L,
|
||||
WRITE = 0x00000001L,
|
||||
READWRITE = 0x00000002L,
|
||||
CREATE = 0x00001000L,
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,11 @@
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="NativeMethods.txt" />
|
||||
<AdditionalFiles Include="NativeMethods.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false,
|
||||
"comInterop": {
|
||||
"preserveSigMethods": [ "*" ]
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,25 @@ public class WindowsPackageManagerStandardFactory : WindowsPackageManagerFactory
|
||||
protected override T CreateInstance<T>(Guid clsid, Guid iid)
|
||||
{
|
||||
var pUnknown = IntPtr.Zero;
|
||||
try
|
||||
unsafe
|
||||
{
|
||||
var hr = PInvoke.CoCreateInstance(clsid, null, CLSCTX.CLSCTX_ALL, iid, out var result);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
pUnknown = Marshal.GetIUnknownForObject(result);
|
||||
return MarshalGeneric<T>.FromAbi(pUnknown);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// CoCreateInstance and FromAbi both AddRef on the native object.
|
||||
// Release once to prevent memory leak.
|
||||
if (pUnknown != IntPtr.Zero)
|
||||
try
|
||||
{
|
||||
Marshal.Release(pUnknown);
|
||||
var hr = PInvoke.CoCreateInstance(clsid, null, CLSCTX.CLSCTX_ALL, iid, out var result);
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
|
||||
pUnknown = new IntPtr(result);
|
||||
|
||||
return MarshalGeneric<T>.FromAbi(pUnknown);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// CoCreateInstance and FromAbi both AddRef on the native object.
|
||||
// Release once to prevent memory leak.
|
||||
if (pUnknown != IntPtr.Zero)
|
||||
{
|
||||
Marshal.Release(pUnknown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ internal sealed class ContextMenuHelper
|
||||
contextMenu.Add(new CommandContextItem(new KillProcessCommand(windowData))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(true, false, false, false, (int)VirtualKey.Delete, 0),
|
||||
IsCritical = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||
|
||||
@@ -11,18 +12,16 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||
/// Interface for accessing Virtual Desktop Manager.
|
||||
/// Code used from <see href="https://learn.microsoft.com/archive/blogs/winsdk/virtual-desktop-switching-in-windows-10"./>
|
||||
/// </summary>
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[GeneratedComInterface]
|
||||
[Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
|
||||
[System.Security.SuppressUnmanagedCodeSecurity]
|
||||
internal interface IVirtualDesktopManager
|
||||
public partial interface IVirtualDesktopManager
|
||||
{
|
||||
[PreserveSig]
|
||||
int IsWindowOnCurrentVirtualDesktop([In] IntPtr hTopLevelWindow, [Out] out int onCurrentDesktop);
|
||||
int IsWindowOnCurrentVirtualDesktop(IntPtr hTopLevelWindow, out int onCurrentDesktop);
|
||||
|
||||
[PreserveSig]
|
||||
int GetWindowDesktopId([In] IntPtr hTopLevelWindow, [Out] out Guid desktop);
|
||||
int GetWindowDesktopId(IntPtr hTopLevelWindow, out Guid desktop);
|
||||
|
||||
[PreserveSig]
|
||||
int MoveWindowToDesktop([In] IntPtr hTopLevelWindow, [MarshalAs(UnmanagedType.LPStruct)][In] Guid desktop);
|
||||
int MoveWindowToDesktop(IntPtr hTopLevelWindow, ref Guid desktop);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessage
|
||||
namespace Microsoft.CmdPal.Ext.WindowWalker.Helpers;
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1401:P/Invokes should not be visible", Justification = "We want plugins to share this NativeMethods class, instead of each one creating its own.")]
|
||||
public static class NativeMethods
|
||||
public static partial class NativeMethods
|
||||
{
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern int EnumWindows(EnumWindowsProc callPtr, IntPtr lParam);
|
||||
@@ -99,31 +99,14 @@ public static class NativeMethods
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetFirmwareType(ref FirmwareType FirmwareType);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool ExitWindowsEx(uint uFlags, uint dwReason);
|
||||
|
||||
[DllImport("user32")]
|
||||
public static extern void LockWorkStation();
|
||||
|
||||
[DllImport("Powrprof.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
|
||||
|
||||
[DllImport("Shell32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags);
|
||||
|
||||
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern HRESULT SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, uint cchOutBuf, IntPtr ppvReserved);
|
||||
|
||||
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern HRESULT SHCreateStreamOnFileEx(string fileName, STGM grfMode, uint attributes, bool create, System.Runtime.InteropServices.ComTypes.IStream reserved, out System.Runtime.InteropServices.ComTypes.IStream stream);
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string path, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
||||
|
||||
[DllImport("rpcrt4.dll")]
|
||||
public static extern int UuidCreateSequential(out GUIDDATA Uuid);
|
||||
[LibraryImport("ole32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.U4)]
|
||||
public static partial uint CoCreateInstance(
|
||||
ref Guid rclsid,
|
||||
IntPtr pUnkOuter,
|
||||
uint dwClsContext,
|
||||
ref Guid riid,
|
||||
out IntPtr rReturnedComObject);
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "These are the names used by win32.")]
|
||||
@@ -161,19 +144,6 @@ public static class Win32Constants
|
||||
public const int RPC_S_UUID_LOCAL_ONLY = 0x720;
|
||||
}
|
||||
|
||||
public static class ShellItemTypeConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Guid for type IShellItem.
|
||||
/// </summary>
|
||||
public static readonly Guid ShellItemGuid = new("43826d1e-e718-42ee-bc55-a1e261c37bfe");
|
||||
|
||||
/// <summary>
|
||||
/// Guid for type IShellItem2.
|
||||
/// </summary>
|
||||
public static readonly Guid ShellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93");
|
||||
}
|
||||
|
||||
public enum HRESULT : uint
|
||||
{
|
||||
/// <summary>
|
||||
@@ -1124,26 +1094,6 @@ public enum ExtendedWindowStyles : uint
|
||||
WS_EX_NOACTIVATE = 0x8000000,
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
|
||||
public interface IShellItem
|
||||
{
|
||||
void BindToHandler(
|
||||
IntPtr pbc,
|
||||
[MarshalAs(UnmanagedType.LPStruct)] Guid bhid,
|
||||
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
|
||||
out IntPtr ppv);
|
||||
|
||||
void GetParent(out IShellItem ppsi);
|
||||
|
||||
void GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
|
||||
|
||||
void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
||||
|
||||
void Compare(IShellItem psi, uint hint, out int piOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The following are ShellItem DisplayName types.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using System.Text;
|
||||
|
||||
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
|
||||
@@ -48,6 +49,10 @@ public class VirtualDesktopHelper
|
||||
/// </summary>
|
||||
private readonly List<Guid> _availableDesktops = [];
|
||||
|
||||
#pragma warning disable SA1306 // Field names should begin with lower-case letter
|
||||
private readonly uint CLSCTXINPROCALL = 0x17;
|
||||
#pragma warning restore SA1306 // Field names should begin with lower-case letter
|
||||
|
||||
/// <summary>
|
||||
/// Id of the current visible Desktop.
|
||||
/// </summary>
|
||||
@@ -55,15 +60,27 @@ public class VirtualDesktopHelper
|
||||
|
||||
private static readonly CompositeFormat VirtualDesktopHelperDesktop = System.Text.CompositeFormat.Parse(Properties.Resources.VirtualDesktopHelper_Desktop);
|
||||
|
||||
private Guid iVirtualDesktopManagerCLSID = new("aa509086-5ca9-4c25-8f95-589d3c07b48a");
|
||||
|
||||
private Guid iVirtualDesktopManagerIID = new("a5cd92ff-29be-454c-8d04-d82879fb3f1b");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VirtualDesktopHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="desktopListUpdate">Setting to configure if the list of available desktops should update automatically or only when calling <see cref="UpdateDesktopList"/>. Per default this is set to manual update (false) to have less registry queries.</param>
|
||||
public VirtualDesktopHelper(bool desktopListUpdate = false)
|
||||
{
|
||||
var cw = new StrategyBasedComWrappers();
|
||||
|
||||
try
|
||||
{
|
||||
_virtualDesktopManager = (IVirtualDesktopManager)new CVirtualDesktopManager();
|
||||
var hr = NativeMethods.CoCreateInstance(ref this.iVirtualDesktopManagerCLSID, nint.Zero, CLSCTXINPROCALL, ref iVirtualDesktopManagerIID, out var virtualDesktopManagerPtr);
|
||||
if (hr != 0)
|
||||
{
|
||||
throw new ArgumentException($"Failed to create IVirtualDesktopManager instance. HR: 0x{hr:X}");
|
||||
}
|
||||
|
||||
_virtualDesktopManager = (IVirtualDesktopManager)cw.GetOrCreateObjectForComInstance(virtualDesktopManagerPtr, CreateObjectFlags.None);
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
@@ -409,7 +426,7 @@ public class VirtualDesktopHelper
|
||||
/// <param name="hWindow">Handle of the top level window.</param>
|
||||
/// <param name="desktopId">Guid of the target desktop.</param>
|
||||
/// <returns><see langword="True"/> on success and <see langword="false"/> on failure.</returns>
|
||||
public bool MoveWindowToDesktop(IntPtr hWindow, in Guid desktopId)
|
||||
public bool MoveWindowToDesktop(IntPtr hWindow, ref Guid desktopId)
|
||||
{
|
||||
if (_virtualDesktopManager == null)
|
||||
{
|
||||
@@ -417,7 +434,7 @@ public class VirtualDesktopHelper
|
||||
return false;
|
||||
}
|
||||
|
||||
var hr = _virtualDesktopManager.MoveWindowToDesktop(hWindow, desktopId);
|
||||
var hr = _virtualDesktopManager.MoveWindowToDesktop(hWindow, ref desktopId);
|
||||
if (hr != (int)HRESULT.S_OK)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "VirtualDesktopHelper.MoveWindowToDesktop() failed: An exception was thrown when moving the window ({hWindow}) to another desktop ({desktopId})." });
|
||||
@@ -455,7 +472,7 @@ public class VirtualDesktopHelper
|
||||
}
|
||||
|
||||
Guid newDesktop = _availableDesktops[windowDesktopNumber - 1];
|
||||
return MoveWindowToDesktop(hWindow, newDesktop);
|
||||
return MoveWindowToDesktop(hWindow, ref newDesktop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -486,7 +503,7 @@ public class VirtualDesktopHelper
|
||||
}
|
||||
|
||||
Guid newDesktop = _availableDesktops[windowDesktopNumber + 1];
|
||||
return MoveWindowToDesktop(hWindow, newDesktop);
|
||||
return MoveWindowToDesktop(hWindow, ref newDesktop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Resources;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WindowsTerminal.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Commands;
|
||||
|
||||
@@ -68,7 +62,23 @@ internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand
|
||||
|
||||
private void Launch(string id, string profile)
|
||||
{
|
||||
var appManager = new ApplicationActivationManager();
|
||||
ComWrappers cw = new StrategyBasedComWrappers();
|
||||
|
||||
var hr = NativeHelpers.CoCreateInstance(ref NativeHelpers.ApplicationActivationManagerCLSID, IntPtr.Zero, NativeHelpers.CLSCTXINPROCALL, ref NativeHelpers.ApplicationActivationManagerIID, out var appManagerPtr);
|
||||
|
||||
if (hr != 0)
|
||||
{
|
||||
throw new ArgumentException($"Failed to create IApplicationActivationManager instance. HR: 0x{hr:X}");
|
||||
}
|
||||
|
||||
var appManager = (IApplicationActivationManager)cw.GetOrCreateObjectForComInstance(
|
||||
appManagerPtr, CreateObjectFlags.None);
|
||||
|
||||
if (appManager == null)
|
||||
{
|
||||
throw new ArgumentException("Failed to get IApplicationActivationManager interface");
|
||||
}
|
||||
|
||||
const ActivateOptions noFlags = ActivateOptions.None;
|
||||
var queryArguments = TerminalHelper.GetArguments(profile, _openNewTab, _openQuake);
|
||||
try
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Resources;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WindowsTerminal.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Commands;
|
||||
|
||||
@@ -38,7 +32,22 @@ internal sealed partial class LaunchProfileCommand : InvokableCommand
|
||||
|
||||
private void Launch(string id, string profile)
|
||||
{
|
||||
var appManager = new ApplicationActivationManager();
|
||||
ComWrappers cw = new StrategyBasedComWrappers();
|
||||
|
||||
var hr = NativeHelpers.CoCreateInstance(ref NativeHelpers.ApplicationActivationManagerCLSID, IntPtr.Zero, NativeHelpers.CLSCTXINPROCALL, ref NativeHelpers.ApplicationActivationManagerIID, out var appManagerPtr);
|
||||
if (hr != 0)
|
||||
{
|
||||
throw new ArgumentException($"Failed to create IApplicationActivationManager instance. HR: 0x{hr:X}");
|
||||
}
|
||||
|
||||
var appManager = (IApplicationActivationManager)cw.GetOrCreateObjectForComInstance(
|
||||
appManagerPtr, CreateObjectFlags.None);
|
||||
|
||||
if (appManager == null)
|
||||
{
|
||||
throw new ArgumentException("Failed to get IApplicationActivationManager interface");
|
||||
}
|
||||
|
||||
const ActivateOptions noFlags = ActivateOptions.None;
|
||||
var queryArguments = TerminalHelper.GetArguments(profile, _openNewTab, _openQuake);
|
||||
try
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
|
||||
|
||||
// Application Activation Manager Class
|
||||
[ComImport]
|
||||
[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
|
||||
public class ApplicationActivationManager : IApplicationActivationManager
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
|
||||
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
|
||||
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
|
||||
|
||||
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
|
||||
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.Marshalling;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers;
|
||||
|
||||
@@ -18,14 +19,13 @@ public enum ActivateOptions
|
||||
}
|
||||
|
||||
// ApplicationActivationManager
|
||||
[ComImport]
|
||||
[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
|
||||
[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IApplicationActivationManager
|
||||
public partial interface IApplicationActivationManager
|
||||
{
|
||||
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);
|
||||
void ActivateApplication(string appUserModelId, string arguments, ActivateOptions options, out uint processId);
|
||||
|
||||
IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);
|
||||
void ActivateForFile(string appUserModelId, IntPtr /*IShellItemArray* */ itemArray, string verb, out uint processId);
|
||||
|
||||
IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
|
||||
void ActivateForProtocol(string appUserModelId, IntPtr /* IShellItemArray* */itemArray, out uint processId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.CmdPal.Ext.WindowsTerminal.Helpers;
|
||||
|
||||
public sealed partial class NativeHelpers
|
||||
{
|
||||
public const uint CLSCTXINPROCALL = 0x17;
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
public static partial uint CoCreateInstance(
|
||||
ref Guid rclsid,
|
||||
IntPtr pUnkOuter,
|
||||
uint dwClsContext,
|
||||
ref Guid riid,
|
||||
out IntPtr rReturnedComObject);
|
||||
|
||||
#pragma warning disable CA2211 // Non-constant fields should not be visible
|
||||
#pragma warning disable SA1401 // Fields should be private
|
||||
public static Guid ApplicationActivationManagerCLSID = new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
|
||||
public static Guid ApplicationActivationManagerIID = new Guid("2e941141-7f97-4756-ba1d-9decde894a3d");
|
||||
#pragma warning restore SA1401 // Fields should be private
|
||||
#pragma warning restore CA2211 // Non-constant fields should not be visible
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<PathToRoot>..\..\..\..\..\</PathToRoot>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001</WasdkNuget>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250513003</WasdkNuget>
|
||||
<CppWinRTNuget>$(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5</CppWinRTNuget>
|
||||
<WindowsSdkBuildToolsNuget>$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428</WindowsSdkBuildToolsNuget>
|
||||
<WebView2Nuget>$(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40</WebView2Nuget>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2903.40" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.7.250401001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.7.250513003" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-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>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\FancyZones.FuzzTests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\editor\FancyZonesEditor\Utils\ParsingResult.cs" Link="ParsingResult.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\CustomLayouts.cs" Link="CustomLayouts.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\EditorData`1.cs" Link="EditorData`1.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\LayoutDefaultSettings.cs" Link="LayoutDefaultSettings.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Utils\DashCaseNamingPolicy.cs" Link="DashCaseNamingPolicy.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Utils\IOUtils.cs" Link="IOUtils.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Utils\StringUtils.cs" Link="StringUtils.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="OneFuzzConfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
86
src/modules/fancyzones/FancyZones.FuzzTests/FuzzTests.cs
Normal file
@@ -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.Text.Json;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
using static FancyZonesEditorCommon.Data.CustomLayouts;
|
||||
|
||||
namespace FancyZones.FuzzTests
|
||||
{
|
||||
public class FuzzTests
|
||||
{
|
||||
public static void FuzzGridFromJsonElement(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length < 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int inputData = BitConverter.ToInt32(input.Slice(0, 4));
|
||||
|
||||
// mock user input for custom-layouts.json
|
||||
string mockCustomLayouts = $@"{{""custom-layouts"": [{{
|
||||
""uuid"": ""{{B8C275E-A7BC-485F-A35C-67B69164F51F}}"",
|
||||
""name"": ""Custom layout 1"",
|
||||
""type"": ""grid"",
|
||||
""info"": {{
|
||||
""rows"": {inputData},
|
||||
""columns"": {inputData},
|
||||
""rows-percentage"": [ {inputData} ],
|
||||
""columns-percentage"": [ {inputData}, {inputData}, {inputData} ],
|
||||
""cell-child-map"": [ [{inputData}, {inputData}, {inputData}] ],
|
||||
""show-spacing"": true,
|
||||
""spacing"": {inputData},
|
||||
""sensitivity-radius"": {inputData}
|
||||
}}
|
||||
}}]}}";
|
||||
|
||||
CustomLayoutListWrapper wrapper;
|
||||
try
|
||||
{
|
||||
wrapper = JsonSerializer.Deserialize<CustomLayoutListWrapper>(mockCustomLayouts, JsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<CustomLayouts.CustomLayoutWrapper> customLayouts = wrapper.CustomLayouts;
|
||||
|
||||
if (customLayouts == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Layout Info from mockCustomLayouts
|
||||
foreach (var zoneSet in customLayouts)
|
||||
{
|
||||
if (zoneSet.Uuid == null || zoneSet.Uuid.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CustomLayouts deserializer = new CustomLayouts();
|
||||
|
||||
// Fuzzing the deserializer
|
||||
_ = deserializer.GridFromJsonElement(zoneSet.Info.GetRawText());
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions JsonOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
return new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = new DashCaseNamingPolicy(),
|
||||
WriteIndented = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||