330 lines
14 KiB
PowerShell
330 lines
14 KiB
PowerShell
# End-to-end test: summon modes (click / long-press / hold), execute-by-click, swipe select, mouse position, global switch.
|
|
param(
|
|
[string]$Exe = "src/OneClickRun.App/bin/x64/Debug/net8.0-windows10.0.19041.0/win-x64/OneClickRun.exe"
|
|
)
|
|
$ErrorActionPreference = "Stop"
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$exePath = Join-Path $root $Exe
|
|
if (-not (Test-Path $exePath)) { throw "App not found: $exePath" }
|
|
|
|
$settingsDir = Join-Path $env:APPDATA "OneClickRun"
|
|
$settingsFile = Join-Path $settingsDir "settings.json"
|
|
$settingsBackup = $null
|
|
if (Test-Path $settingsFile) {
|
|
$settingsBackup = $settingsFile + ".e2e-backup"
|
|
Copy-Item $settingsFile $settingsBackup -Force
|
|
}
|
|
|
|
Add-Type @"
|
|
using System;
|
|
using System.Text;
|
|
using System.Runtime.InteropServices;
|
|
public class E2EWin {
|
|
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
|
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
|
|
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
|
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
|
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);
|
|
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
|
}
|
|
"@
|
|
|
|
$script:Proc = $null
|
|
$script:TargetPid = 0
|
|
$script:WheelHwnd = [IntPtr]::Zero
|
|
$script:WheelRect = New-Object E2EWin+RECT
|
|
$script:WheelVisible = $false
|
|
|
|
function Get-WheelInfo {
|
|
$script:WheelVisible = $false
|
|
$script:WheelHwnd = [IntPtr]::Zero
|
|
$cb = [E2EWin+EnumWindowsProc]{
|
|
param($h, $l)
|
|
$wpid = 0
|
|
[void][E2EWin]::GetWindowThreadProcessId($h, [ref]$wpid)
|
|
if ($wpid -eq $script:TargetPid) {
|
|
$sb = New-Object System.Text.StringBuilder 256
|
|
[void][E2EWin]::GetWindowText($h, $sb, 256)
|
|
if ($sb.ToString() -like '*OneClickRun Wheel*') {
|
|
$script:WheelHwnd = $h
|
|
$rect = New-Object E2EWin+RECT
|
|
[void][E2EWin]::GetWindowRect($h, [ref]$rect)
|
|
$script:WheelRect = $rect
|
|
$script:WheelVisible = [E2EWin]::IsWindowVisible($h)
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
[void][E2EWin]::EnumWindows($cb, [IntPtr]::Zero)
|
|
return $script:WheelVisible
|
|
}
|
|
|
|
function Write-Settings([hashtable]$s) {
|
|
$s | ConvertTo-Json -Depth 10 | Set-Content -Path $settingsFile -Encoding UTF8
|
|
}
|
|
|
|
function Start-App {
|
|
$script:Proc = Start-Process -FilePath $exePath -PassThru
|
|
$script:TargetPid = $script:Proc.Id
|
|
Start-Sleep -Seconds 4
|
|
if ($script:Proc.HasExited) { throw "App exited early with code $($script:Proc.ExitCode)" }
|
|
}
|
|
|
|
function Stop-App {
|
|
if ($script:Proc -and -not $script:Proc.HasExited) { Stop-Process -Id $script:TargetPid -Force }
|
|
Start-Sleep -Milliseconds 400
|
|
}
|
|
|
|
function Key-Down([byte]$vk) { [E2EWin]::keybd_event($vk, 0, 0, [UIntPtr]::Zero) }
|
|
function Key-Up([byte]$vk) { [E2EWin]::keybd_event($vk, 0, 2, [UIntPtr]::Zero) }
|
|
|
|
function Press-Hotkey { Key-Down 0x11; Key-Down 0x12; Key-Down 0x51; Start-Sleep -Milliseconds 120; Key-Up 0x51; Key-Up 0x12; Key-Up 0x11 }
|
|
function Hold-Hotkey-Down { Key-Down 0x11; Key-Down 0x12; Key-Down 0x51 }
|
|
function Hold-Hotkey-Up { Key-Up 0x51; Key-Up 0x12; Key-Up 0x11 }
|
|
function Press-Esc { Key-Down 0x1B; Start-Sleep -Milliseconds 60; Key-Up 0x1B }
|
|
|
|
function Assert([bool]$cond, [string]$msg) { if (-not $cond) { throw "Assertion failed: $msg" } }
|
|
|
|
function Get-SectorPoint([int]$index, [int]$count) {
|
|
$w = $script:WheelRect.Right - $script:WheelRect.Left
|
|
$scale = $w / 560.0
|
|
$cx = ($script:WheelRect.Left + $script:WheelRect.Right) / 2.0
|
|
$cy = ($script:WheelRect.Top + $script:WheelRect.Bottom) / 2.0
|
|
$span = 360.0 / $count
|
|
$mid = ($index + 0.5) * $span
|
|
$rad = $mid * [Math]::PI / 180.0
|
|
$r = 177.5 * $scale
|
|
return @{ X = [int][Math]::Round($cx + $r * [Math]::Sin($rad)); Y = [int][Math]::Round($cy - $r * [Math]::Cos($rad)) }
|
|
}
|
|
|
|
function Click-At([int]$x, [int]$y) {
|
|
[void][E2EWin]::SetCursorPos($x, $y)
|
|
Start-Sleep -Milliseconds 120
|
|
[E2EWin]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
Start-Sleep -Milliseconds 80
|
|
[E2EWin]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
|
|
function Click-Once([int]$x, [int]$y) {
|
|
[void][E2EWin]::SetCursorPos($x, $y)
|
|
Start-Sleep -Milliseconds 60
|
|
[E2EWin]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
|
Start-Sleep -Milliseconds 40
|
|
[E2EWin]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
|
Start-Sleep -Milliseconds 30
|
|
}
|
|
|
|
function New-NotepadCommand { @{ Name = "Notepad"; ActionType = 0; Target = (Join-Path $env:WINDIR 'System32\notepad.exe'); IconGlyph = "A"; ColorIndex = 0 } }
|
|
function New-CalcCommand { @{ Name = "Calc"; ActionType = 0; Target = (Join-Path $env:WINDIR 'System32\calc.exe'); IconGlyph = "B"; ColorIndex = 1 } }
|
|
function New-BingCommand { @{ Name = "Bing"; ActionType = 2; Target = "https://www.bing.com"; IconGlyph = "C"; ColorIndex = 2 } }
|
|
|
|
function New-BaseSettings([int]$summon, [int]$selection, [int]$position, [bool]$enabled, [bool]$startWithWindows = $false) {
|
|
return @{
|
|
SchemaVersion = 1; StartWithWindows = $startWithWindows; WheelEnabled = $enabled
|
|
WheelPosition = $position; Hotkey = @{ Modifiers = 3; Key = 81 }
|
|
SummonMode = $summon; LongPressDelayMs = 400; SelectionMode = $selection; ThemeMode = 2
|
|
Commands = @((New-NotepadCommand), (New-CalcCommand), (New-BingCommand))
|
|
}
|
|
}
|
|
|
|
$failed = 0
|
|
$results = New-Object System.Collections.Generic.List[string]
|
|
|
|
function Run-Test([string]$name, [scriptblock]$body) {
|
|
try {
|
|
& $body
|
|
$results.Add("PASS $name")
|
|
Write-Host "PASS $name"
|
|
} catch {
|
|
$script:failed++
|
|
$results.Add("FAIL $name :: $($_.Exception.Message)")
|
|
Write-Host "FAIL $name :: $($_.Exception.Message)" -ForegroundColor Red
|
|
} finally {
|
|
Stop-App
|
|
}
|
|
}
|
|
|
|
try {
|
|
Run-Test "click summon: show then toggle-hide" {
|
|
Remove-Item $settingsFile -ErrorAction SilentlyContinue
|
|
Start-App
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (-not (Get-WheelInfo)) "second hotkey press did not hide wheel"
|
|
}
|
|
|
|
Run-Test "click summon: Esc dismisses wheel" {
|
|
Remove-Item $settingsFile -ErrorAction SilentlyContinue
|
|
Start-App
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
Press-Esc; Start-Sleep -Milliseconds 500
|
|
Assert (-not (Get-WheelInfo)) "wheel still visible after Esc"
|
|
}
|
|
|
|
Run-Test "click execute: sector 0 starts Notepad" {
|
|
Remove-Item $settingsFile -ErrorAction SilentlyContinue
|
|
Start-App
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
$p = Get-SectorPoint 0 5
|
|
Click-At $p.X $p.Y
|
|
Start-Sleep -Milliseconds 800
|
|
Assert (-not (Get-WheelInfo)) "wheel not hidden after execute"
|
|
$np = Get-Process notepad -ErrorAction SilentlyContinue
|
|
Assert ($null -ne $np) "Notepad process was not started"
|
|
$np | Stop-Process -Force
|
|
}
|
|
|
|
Run-Test "long-press summon: shows after 400ms and stays after release" {
|
|
Write-Settings (New-BaseSettings 1 0 0 $true)
|
|
Start-App
|
|
Hold-Hotkey-Down
|
|
Start-Sleep -Milliseconds 300
|
|
Assert (-not (Get-WheelInfo)) "wheel shown too early (300ms)"
|
|
Start-Sleep -Milliseconds 250
|
|
Assert (Get-WheelInfo) "wheel not shown after 400ms long press"
|
|
Hold-Hotkey-Up
|
|
Start-Sleep -Milliseconds 400
|
|
Assert (Get-WheelInfo) "wheel disappeared after release (should stay)"
|
|
Press-Esc; Start-Sleep -Milliseconds 400
|
|
Assert (-not (Get-WheelInfo)) "Esc did not close wheel"
|
|
}
|
|
|
|
Run-Test "hold summon: visible while held, hidden on release" {
|
|
Write-Settings (New-BaseSettings 2 0 0 $true)
|
|
Start-App
|
|
Hold-Hotkey-Down
|
|
Start-Sleep -Milliseconds 400
|
|
Assert (Get-WheelInfo) "wheel not visible while hotkey held"
|
|
Hold-Hotkey-Up
|
|
Start-Sleep -Milliseconds 400
|
|
Assert (-not (Get-WheelInfo)) "wheel still visible after release"
|
|
}
|
|
|
|
Run-Test "swipe select: hold + mouse up, release executes Notepad" {
|
|
Write-Settings (New-BaseSettings 2 2 0 $true)
|
|
Start-App
|
|
Hold-Hotkey-Down
|
|
Start-Sleep -Milliseconds 400
|
|
Assert (Get-WheelInfo) "wheel not visible while hotkey held"
|
|
$p = Get-SectorPoint 0 3
|
|
[void][E2EWin]::SetCursorPos($p.X, $p.Y)
|
|
Start-Sleep -Milliseconds 150
|
|
Hold-Hotkey-Up
|
|
Start-Sleep -Milliseconds 800
|
|
Assert (-not (Get-WheelInfo)) "wheel still visible after release"
|
|
$np = Get-Process notepad -ErrorAction SilentlyContinue
|
|
Assert ($null -ne $np) "swipe did not execute Notepad"
|
|
$np | Stop-Process -Force
|
|
}
|
|
|
|
Run-Test "wheel position: mouse cursor" {
|
|
Write-Settings (New-BaseSettings 0 0 1 $true)
|
|
Start-App
|
|
[void][E2EWin]::SetCursorPos(400, 300)
|
|
Start-Sleep -Milliseconds 150
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
$cx = ($script:WheelRect.Left + $script:WheelRect.Right) / 2.0
|
|
$cy = ($script:WheelRect.Top + $script:WheelRect.Bottom) / 2.0
|
|
$d = [Math]::Sqrt(($cx - 400) * ($cx - 400) + ($cy - 300) * ($cy - 300))
|
|
Assert ($d -lt 12) "wheel center offset from cursor by $([int]$d)px"
|
|
Press-Esc
|
|
}
|
|
|
|
Run-Test "double-click selection: first click pending, second click executes" {
|
|
Write-Settings (New-BaseSettings 0 1 0 $true)
|
|
Start-App
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
$p = Get-SectorPoint 0 3
|
|
Click-Once $p.X $p.Y
|
|
Start-Sleep -Milliseconds 150
|
|
Assert (Get-WheelInfo) "wheel hidden after first click in double-click mode"
|
|
$np = Get-Process notepad -ErrorAction SilentlyContinue
|
|
Assert ($null -eq $np) "Notepad started after single click in double-click mode"
|
|
Click-Once $p.X $p.Y
|
|
Start-Sleep -Milliseconds 600
|
|
Assert (-not (Get-WheelInfo)) "wheel not hidden after double click"
|
|
$np = Get-Process notepad -ErrorAction SilentlyContinue
|
|
Assert ($null -ne $np) "Notepad not started after double click"
|
|
$np | Stop-Process -Force
|
|
}
|
|
|
|
Run-Test "autostart: registry Run value written at launch" {
|
|
Write-Settings (New-BaseSettings 0 0 0 $true $true)
|
|
Start-App
|
|
$runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
|
|
$prev = (Get-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue).OneClickRun
|
|
try {
|
|
$cur = (Get-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue).OneClickRun
|
|
Assert ($null -ne $cur) "autostart registry value missing"
|
|
Assert ($cur -like '*OneClickRun*') "unexpected autostart value: $cur"
|
|
} finally {
|
|
if ($null -eq $prev) {
|
|
Remove-ItemProperty -Path $runKey -Name OneClickRun -ErrorAction SilentlyContinue
|
|
} else {
|
|
Set-ItemProperty -Path $runKey -Name OneClickRun -Value $prev
|
|
}
|
|
}
|
|
}
|
|
|
|
Run-Test "script execute: cmd and ps1 sectors run" {
|
|
$cmdPath = Join-Path $env:TEMP "ocr-e2e-run.cmd"
|
|
$ps1Path = Join-Path $env:TEMP "ocr-e2e-run.ps1"
|
|
$cmdMarker = Join-Path $env:TEMP "ocr-e2e-cmd-marker.txt"
|
|
$ps1Marker = Join-Path $env:TEMP "ocr-e2e-ps1-marker.txt"
|
|
Remove-Item $cmdMarker, $ps1Marker -ErrorAction SilentlyContinue
|
|
$cmdLine = 'echo ok > "' + $cmdMarker + '"'
|
|
$ps1Line = "Set-Content -Path '" + $ps1Marker + "' -Value ok"
|
|
Set-Content -Path $cmdPath -Value @('@echo off', $cmdLine) -Encoding ASCII
|
|
Set-Content -Path $ps1Path -Value $ps1Line -Encoding ASCII
|
|
|
|
$settings = New-BaseSettings 0 0 0 $true
|
|
$settings.Commands = @(
|
|
@{ Name = "RunCmd"; ActionType = 3; Target = $cmdPath; IconGlyph = "D"; ColorIndex = 0 },
|
|
@{ Name = "RunPs1"; ActionType = 3; Target = $ps1Path; IconGlyph = "E"; ColorIndex = 1 }
|
|
)
|
|
Write-Settings $settings
|
|
Start-App
|
|
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not show wheel"
|
|
$p0 = Get-SectorPoint 0 2
|
|
Click-At $p0.X $p0.Y
|
|
Start-Sleep -Milliseconds 1200
|
|
Assert (Test-Path $cmdMarker) "cmd script did not run"
|
|
Assert (-not (Get-WheelInfo)) "wheel not hidden after cmd execute"
|
|
|
|
Press-Hotkey; Start-Sleep -Milliseconds 500
|
|
Assert (Get-WheelInfo) "hotkey did not re-show wheel"
|
|
$p1 = Get-SectorPoint 1 2
|
|
Click-At $p1.X $p1.Y
|
|
Start-Sleep -Milliseconds 1200
|
|
Assert (Test-Path $ps1Marker) "ps1 script did not run"
|
|
|
|
Remove-Item $cmdMarker, $ps1Marker, $cmdPath, $ps1Path -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Run-Test "global switch off: wheel cannot be summoned" {
|
|
Write-Settings (New-BaseSettings 0 0 0 $false)
|
|
Start-App
|
|
Press-Hotkey; Start-Sleep -Milliseconds 600
|
|
Assert (-not (Get-WheelInfo)) "wheel summoned while global switch off"
|
|
}
|
|
} finally {
|
|
Stop-App
|
|
if ($settingsBackup) { Move-Item $settingsBackup $settingsFile -Force } else { Remove-Item $settingsFile -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host ("=== E2E RESULT: {0}/{1} passed ===" -f ($results.Count - $failed), $results.Count)
|
|
exit $failed
|