test(tools): 3b 真机验收脚本 + 修正 IME proof 的按钮定位
rel_mouse_phone_proof.ps1 覆盖 V-B1a(相对位移方向/符号/不跳变)、V-B2(切绝对 模式后 R7 重测)、V-B3(长按出右键、拖拽不出)。 三处 harness 教训(都已写进脚本注释,避免下次重踩): 1. uiautomator 不暴露 SurfaceView,裸 SurfaceView 塌成 android.view.View; 按 16:9 宽高比识别视频视图,并断言比例,避免静默抓错节点。 2. 上一段滑动的相对事件可能在 SetCursorPos 之后才落地,制造出 15px/298px 的 假跳变。判据前必须先等链路静默、并要求基线两次读数一致。 3. ime_chinese_proof 用「第一个 Button」当键盘按钮——3b 在按钮栏上方插入了 灵敏度行,第一个 Button 变成了减号,proof 报「IME not shown」。 改为按文案匹配,与位置无关。
This commit is contained in:
parent
aede7b6c3c
commit
1a6c3f15a2
@ -144,12 +144,27 @@ while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 900; $ui = Get-Ui;
|
||||
if ($ui -notmatch "RTT=") { throw "phone did not connect" }
|
||||
|
||||
Write-Host "=== 4. show IME (tap keyboard button) ==="
|
||||
$btns = [regex]::Matches($ui, '<node[^>]*class="android\.widget\.Button"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($btns.Count -lt 1) { throw "keyboard button not found" }
|
||||
$b0 = $btns[0]
|
||||
$kx = [int](([int]$b0.Groups[1].Value + [int]$b0.Groups[3].Value) / 2)
|
||||
$ky = [int](([int]$b0.Groups[2].Value + [int]$b0.Groups[4].Value) / 2)
|
||||
[void](Adb-Shell @("input","tap","$kx","$ky"))
|
||||
# Select the keyboard button BY ITS LABEL, not by position.
|
||||
# 3b added a sensitivity row (- / +) above the button bar, so "the first Button"
|
||||
# is now the minus sign - the old positional assumption silently tapped the wrong
|
||||
# control and the proof reported "IME not shown" (a harness defect, not a product
|
||||
# regression; measured 2026-10-31). Label matching is order-independent.
|
||||
$F_KEYBOARD = -join ([char]0x952E,[char]0x76D8) # "keyboard"
|
||||
$kb = $null
|
||||
foreach ($n in [regex]::Matches($ui,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t -eq $F_KEYBOARD) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$kb = @(
|
||||
[int](([int]$b.Groups[1].Value + [int]$b.Groups[3].Value) / 2),
|
||||
[int](([int]$b.Groups[2].Value + [int]$b.Groups[4].Value) / 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $kb) { throw "keyboard button not found by label" }
|
||||
[void](Adb-Shell @("input","tap","$($kb[0])","$($kb[1])"))
|
||||
Start-Sleep -Seconds 3
|
||||
$ime = (Adb-Shell @("dumpsys","input_method")).out
|
||||
if ($ime -notmatch "mInputShown=true") { throw "IME not shown" }
|
||||
|
||||
504
tools/rel_mouse_phone_proof.ps1
Normal file
504
tools/rel_mouse_phone_proof.ps1
Normal file
@ -0,0 +1,504 @@
|
||||
# 3b phone-side proof: relative mouse (protocol v4) + long-press right click + R7 regression.
|
||||
#
|
||||
# ASCII-only source (PS 5.1 decodes BOM-less .ps1 as GBK; see personal lessons).
|
||||
# Expected Chinese UI strings are matched as CODE-POINT fragments (R-8: assert the
|
||||
# actual value, and print what the parser really read so an empty result cannot pass).
|
||||
#
|
||||
# Judgements:
|
||||
# V-B1a relative mode moves the PC pointer by the expected amount (direction+sign)
|
||||
# V-B2 absolute mode still maps finger -> desktop 1:1 (R7 re-run, <=8px error)
|
||||
# V-B3 long press (>=500ms, still) opens a context menu; a drag does NOT
|
||||
# V-B7 R7 + Chinese IME + full chain regressions stay green
|
||||
#
|
||||
# Prereqs: vivo device online via adb, 3b APK installed, v4 agent running.
|
||||
# powershell -ExecutionPolicy Bypass -File tools\rel_mouse_phone_proof.ps1 -Host_ 192.168.1.110
|
||||
param(
|
||||
[string]$Host_ = "192.168.1.110",
|
||||
[int]$Port = 47800,
|
||||
[string]$Code = "t",
|
||||
[string]$AgentExe = ""
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = (Resolve-Path "$PSScriptRoot\..").Path
|
||||
$adb = "D:\working\android\sdk\platform-tools\adb.exe"
|
||||
$ev = Join-Path (Split-Path $repo -Parent) "evidence\3b"
|
||||
New-Item -ItemType Directory -Force -Path $ev | Out-Null
|
||||
|
||||
# ---- code-point fragments (grep-verified present in the Kotlin sources) ----
|
||||
$F_CONNECT = -join ([char]0x8FDE,[char]0x63A5) # "connect"
|
||||
$F_RECONNECT = -join ([char]0x91CD,[char]0x65B0,[char]0x8FDE,[char]0x63A5) # "reconnect"
|
||||
$F_RELATIVE = -join ([char]0x76F8,[char]0x5BF9) # "relative"
|
||||
$F_ABSOLUTE = -join ([char]0x7EDD,[char]0x5BF9) # "absolute"
|
||||
$F_MODE_TAG = -join ([char]0x6A21,[char]0x5F0F) # "mode"
|
||||
$F_LONGPRESS = -join ([char]0x957F,[char]0x6309,[char]0x53F3,[char]0x952E) # "long-press right click"
|
||||
$F_RCLICK_TAG = -join ([char]0x53F3,[char]0x952E,[char]0x3D) # "right-click="
|
||||
$F_CLICK = -join ([char]0x5355,[char]0x51FB) # "single click"
|
||||
|
||||
# Read the persistent right-click counter from the HUD ("右键=N(source)").
|
||||
# Returns -1 when absent. A counter is used instead of the one-line debug text
|
||||
# because ACTION_UP immediately overwrites the debug line after a long press
|
||||
# (measured 2026-10-31: screen showed "长按结束", the firing evidence was gone).
|
||||
function RightClickCount {
|
||||
$m = [regex]::Match((Get-UiText), 'text="([^"]*RTT=[^"]*)"')
|
||||
if (-not $m.Success) { return -1 }
|
||||
$hud = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value)
|
||||
$c = [regex]::Match($hud, "$F_RCLICK_TAG(\d+)")
|
||||
if ($c.Success) { return [int]$c.Groups[1].Value }
|
||||
return 0
|
||||
}
|
||||
|
||||
function Adb {
|
||||
param([Parameter(Mandatory)][string[]]$AdbArgs)
|
||||
$old = $ErrorActionPreference; $ErrorActionPreference = "Continue"
|
||||
try { $out = & $adb @AdbArgs 2>$null; @{ out = ($out -join "`n"); code = $LASTEXITCODE } }
|
||||
finally { $ErrorActionPreference = $old }
|
||||
}
|
||||
function AdbShell {
|
||||
param([string[]]$Cmd)
|
||||
(Adb -AdbArgs (@("shell") + $Cmd)).out
|
||||
}
|
||||
function Get-UiText {
|
||||
$d = AdbShell @("uiautomator","dump","/sdcard/rm.xml")
|
||||
$r = Adb @("pull","/sdcard/rm.xml","$env:TEMP\rm.xml")
|
||||
if ($r.code -ne 0 -or -not (Test-Path "$env:TEMP\rm.xml")) {
|
||||
Write-Host " [ui] dump=[$($d -replace "`n",' ')] pull_rc=$($r.code) -> empty"
|
||||
return ""
|
||||
}
|
||||
$txt = [System.IO.File]::ReadAllText("$env:TEMP\rm.xml", [System.Text.Encoding]::UTF8)
|
||||
if ($txt.Length -lt 200) {
|
||||
Write-Host " [ui] short xml ($($txt.Length) chars): $($txt.Substring(0,[Math]::Min(120,$txt.Length)))"
|
||||
}
|
||||
return $txt
|
||||
}
|
||||
function VisibleTexts([string]$xml) {
|
||||
[regex]::Matches($xml,'text="([^"]+)"') | ForEach-Object { [System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) }
|
||||
}
|
||||
function FormFields([string]$xml) {
|
||||
[regex]::Matches($xml,'<node[^>]*class="android\.widget\.EditText"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
}
|
||||
function Center($m) {
|
||||
$a=[int]$m.Groups[1].Value; $b=[int]$m.Groups[2].Value
|
||||
$c=[int]$m.Groups[3].Value; $d=[int]$m.Groups[4].Value
|
||||
@([int](($a+$c)/2), [int](($b+$d)/2))
|
||||
}
|
||||
function ButtonCenter([string]$xml, [string]$want) {
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t -eq $want) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
|
||||
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
|
||||
return @([int](($x1+$x2)/2), [int](($y1+$y2)/2))
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
function ButtonStartingWith([string]$xml, [string]$prefix) {
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t.StartsWith($prefix)) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
|
||||
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
|
||||
return @([int](($x1+$x2)/2), [int](($y1+$y2)/2))
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Toggle the pointer mode and PROVE it flipped by re-reading the button label.
|
||||
# The bottom bar sits at y=2256..2324, right against the system navigation bar;
|
||||
# a single tap there is not reliable (measured 2026-10-31: the tap landed but the
|
||||
# mode did not change). So: tap, wait, re-read, retry; and judge on the BUTTON
|
||||
# label as well as the HUD (two independent observations of the same state).
|
||||
function Set-Mode([string]$want) {
|
||||
# $want is "relative" or "absolute"
|
||||
$wantTag = if ($want -eq "relative") { $F_RELATIVE } else { $F_ABSOLUTE }
|
||||
for ($try=0; $try -lt 4; $try++) {
|
||||
$xml = Get-UiText
|
||||
$cur = $null
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.widget\.Button"[^>]*>')) {
|
||||
$t = [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
if ($t.Contains($F_MODE_TAG)) {
|
||||
$b = [regex]::Match($n.Value,'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
|
||||
if ($b.Success) {
|
||||
$x1=[int]$b.Groups[1].Value; $y1=[int]$b.Groups[2].Value
|
||||
$x2=[int]$b.Groups[3].Value; $y2=[int]$b.Groups[4].Value
|
||||
$cur = @{ label=$t; cx=[int](($x1+$x2)/2); cy=[int](($y1+$y2)/2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $cur) { throw "mode button not found (no Button whose text contains '$F_MODE_TAG')" }
|
||||
Write-Host " mode button label now: [$($cur.label)] (try $try)"
|
||||
if ($cur.label.Contains($wantTag)) { return $true }
|
||||
[void](AdbShell @("input","tap","$($cur.cx)","$($cur.cy)"))
|
||||
Start-Sleep -Milliseconds 700
|
||||
}
|
||||
$final = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
throw "mode did not switch to $want after 4 taps; screen: [$final]"
|
||||
}
|
||||
# The video SurfaceView bounds - touch coordinates must land inside it.
|
||||
#
|
||||
# uiautomator does NOT expose SurfaceView as "android.view.SurfaceView"; a bare
|
||||
# SurfaceView with no accessibility node collapses to a plain "android.view.View"
|
||||
# (measured 2026-10-31 on the live device: the only candidate is 1080x607, which
|
||||
# is exactly the 16:9 letterbox of the 1920x1080 desktop inside a 1080-wide stage).
|
||||
# So identify it structurally: the widest View whose aspect ratio matches 16:9.
|
||||
# Assert the ratio instead of assuming it - a silent wrong node would make every
|
||||
# downstream coordinate assertion meaningless.
|
||||
function SurfaceBounds([string]$xml) {
|
||||
$best = $null; $bestErr = [double]::MaxValue
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*class="android\.view\.View"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')) {
|
||||
$x1=[int]$n.Groups[1].Value; $y1=[int]$n.Groups[2].Value
|
||||
$x2=[int]$n.Groups[3].Value; $y2=[int]$n.Groups[4].Value
|
||||
$w = $x2-$x1; $h = $y2-$y1
|
||||
if ($w -lt 300 -or $h -lt 100) { continue }
|
||||
$ratio = $w / [double]$h
|
||||
$err = [Math]::Abs($ratio - (16.0/9.0))
|
||||
if ($err -lt $bestErr) { $bestErr = $err; $best = @($x1,$y1,$x2,$y2) }
|
||||
}
|
||||
if (-not $best) { return $null }
|
||||
# sanity: the chosen node must really be ~16:9, else we grabbed something else
|
||||
$w = $best[2]-$best[0]; $h = $best[3]-$best[1]
|
||||
if ($bestErr -gt 0.25) {
|
||||
throw "no 16:9 view found (best candidate ${w}x${h}, ratio $([Math]::Round($w/[double]$h,3)))"
|
||||
}
|
||||
Write-Host " video view ${w}x${h} (ratio $([Math]::Round($w/[double]$h,3)), 16:9 error $([Math]::Round($bestErr,4)))"
|
||||
return $best
|
||||
}
|
||||
|
||||
function TypeAscii([string]$s) {
|
||||
foreach ($ch in $s.ToUpper().ToCharArray()) {
|
||||
$k = if ($ch -ge '0' -and $ch -le '9') { 7 + [int][string]$ch }
|
||||
elseif ($ch -ge 'A' -and $ch -le 'Z') { 29 + ([int]$ch - [int][char]'A') }
|
||||
elseif ($ch -eq '.') { 56 }
|
||||
else { throw "unsupported char: $ch" }
|
||||
[void](AdbShell @("input","keyevent","$k"))
|
||||
Start-Sleep -Milliseconds 70
|
||||
}
|
||||
}
|
||||
function FocusedIndex([string]$xml) {
|
||||
$i = -1
|
||||
foreach ($n in [regex]::Matches($xml,'<node[^>]*>')) {
|
||||
if ($n.Value -match 'class="android\.widget\.EditText"') {
|
||||
$i++
|
||||
if ($n.Value -match 'focused="true"') { return $i }
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function Focus-Field($c, [int]$wantIdx) {
|
||||
for ($try=0; $try -lt 3; $try++) {
|
||||
[void](AdbShell @("input","tap","$($c[0])","$($c[1])"))
|
||||
$deadline = (Get-Date).AddSeconds(1.5)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 250
|
||||
if ((FocusedIndex (Get-UiText)) -eq $wantIdx) { return }
|
||||
}
|
||||
}
|
||||
throw "tap did not focus field $wantIdx after 3 tries"
|
||||
}
|
||||
function Clear-Field($c, [int]$idx) {
|
||||
Focus-Field $c $idx
|
||||
for ($i=0; $i -lt 40; $i++) { [void](AdbShell @("input","keyevent","67")) }
|
||||
Start-Sleep -Milliseconds 150
|
||||
}
|
||||
function Restart-AppForm {
|
||||
[void](AdbShell @("input","keyevent","224"))
|
||||
Start-Sleep -Milliseconds 300
|
||||
[void](AdbShell @("wm","dismiss-keyguard"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
[void](AdbShell @("am","force-stop","com.verify.remote"))
|
||||
Start-Sleep -Milliseconds 600
|
||||
[void](AdbShell @("am","start","-n","com.verify.remote/.MainActivity"))
|
||||
$deadline = (Get-Date).AddSeconds(8)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$x = Get-UiText
|
||||
if ((FormFields $x).Count -ge 3) { return $x }
|
||||
Start-Sleep -Milliseconds 300
|
||||
}
|
||||
$diag = (VisibleTexts (Get-UiText)) -join " || "
|
||||
throw "connect form did not appear. screen: [$diag]"
|
||||
}
|
||||
function FillAndConnect([string]$xml, [string]$host_, [string]$port_, [string]$code_) {
|
||||
$f = FormFields $xml
|
||||
if ($f.Count -lt 3) { throw "form has $($f.Count) fields" }
|
||||
Clear-Field (Center $f[0]) 0; TypeAscii $host_
|
||||
Clear-Field (Center $f[1]) 1; TypeAscii $port_
|
||||
Clear-Field (Center $f[2]) 2; TypeAscii $code_
|
||||
$expect = @($host_, $port_, $code_)
|
||||
for ($retry=0; $retry -le 2; $retry++) {
|
||||
$x2 = Get-UiText
|
||||
$vals = @()
|
||||
foreach ($n in [regex]::Matches($x2,'<node[^>]*>')) {
|
||||
if ($n.Value -match 'class="android\.widget\.EditText"') {
|
||||
$vals += [System.Net.WebUtility]::HtmlDecode([regex]::Match($n.Value,'text="([^"]*)"').Groups[1].Value)
|
||||
}
|
||||
}
|
||||
Write-Host " form now: $($vals -join ' | ') (try $retry)"
|
||||
if ($vals.Count -lt 3) { throw "expected 3 fields after typing, got $($vals.Count)" }
|
||||
$bad = -1
|
||||
for ($i=0; $i -lt 3; $i++) { if ($vals[$i] -ne $expect[$i]) { $bad = $i; break } }
|
||||
if ($bad -ge 0) {
|
||||
$f = FormFields $x2
|
||||
Clear-Field (Center $f[$bad]) $bad
|
||||
TypeAscii $expect[$bad]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if ($vals[0] -ne $host_) { throw "form input FAIL: host is [$($vals[0])], expected [$host_]" }
|
||||
if ($vals[1] -ne $port_) { throw "form input FAIL: port is [$($vals[1])], expected [$port_]" }
|
||||
if ($vals[2] -ne $code_) { throw "form input FAIL: code is [$($vals[2])], expected [$code_]" }
|
||||
$btn = $null
|
||||
foreach ($want in @($F_CONNECT, $F_RECONNECT)) { $btn = ButtonCenter $x2 $want; if ($btn) { break } }
|
||||
if (-not $btn) { throw "no primary connect/reconnect button found" }
|
||||
[void](AdbShell @("input","tap","$($btn[0])","$($btn[1])"))
|
||||
}
|
||||
function Wait-Live([int]$seconds, [ref]$hud) {
|
||||
$deadline = (Get-Date).AddSeconds($seconds)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$m = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m.Success) { $hud.Value = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value); return $true }
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# ---- PC-side cursor readback (the only honest judge of "pointer moved N px") ----
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class PC {
|
||||
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; }
|
||||
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr ctx);
|
||||
[DllImport("user32.dll")] public static extern int GetSystemMetrics(int i);
|
||||
public static void Dpi() { SetProcessDpiAwarenessContext(new IntPtr(-4)); }
|
||||
public static int[] Get() { POINT p; GetCursorPos(out p); return new int[]{p.X,p.Y}; }
|
||||
public static void Set(int x,int y) { SetCursorPos(x,y); }
|
||||
public static int W() { return GetSystemMetrics(0); }
|
||||
public static int H() { return GetSystemMetrics(1); }
|
||||
}
|
||||
"@
|
||||
[void][PC]::Dpi()
|
||||
$SW = [PC]::W(); $SH = [PC]::H()
|
||||
Write-Host " PC screen ${SW}x${SH} (physical, DPI-aware)"
|
||||
|
||||
Write-Host "=== 0. stale-artifact guard (R-7) + IME disable + keep-awake ==="
|
||||
if (-not $AgentExe) { $AgentExe = Join-Path $repo "target\release\agent.exe" }
|
||||
$ag = @(Get-Process agent -ErrorAction SilentlyContinue)
|
||||
if ($ag.Count -gt 0 -and (Test-Path $AgentExe) -and ((Get-Item $AgentExe).LastWriteTime -gt $ag[0].StartTime)) {
|
||||
Write-Host " running agent predates current build; restarting"
|
||||
Stop-Process -Name agent -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Milliseconds 800; $ag = @()
|
||||
}
|
||||
if ($ag.Count -eq 0) {
|
||||
if (-not (Test-Path $AgentExe)) { throw "missing $AgentExe" }
|
||||
Start-Process $AgentExe -ArgumentList "--port","$Port","--code",$Code -WindowStyle Hidden | Out-Null
|
||||
Start-Sleep -Seconds 1; Write-Host " agent started (v4)"
|
||||
} else { Write-Host " agent running and current" }
|
||||
|
||||
$origIme = ((AdbShell @("settings","get","secure","default_input_method")) -join "").Trim()
|
||||
Write-Host " original IME: [$origIme] (recorded, restored in finally)"
|
||||
if ($origIme -and $origIme -ne "null") { [void](AdbShell @("ime","disable",$origIme)) }
|
||||
[void](AdbShell @("svc","power","stayon","true"))
|
||||
|
||||
$origCursor = [PC]::Get()
|
||||
Write-Host " PC cursor origin ($($origCursor[0]),$($origCursor[1])) (restored in finally)"
|
||||
|
||||
try {
|
||||
# =================================================================
|
||||
Write-Host "=== 1. connect (protocol v4) and confirm HUD shows v4 ==="
|
||||
$x = Restart-AppForm
|
||||
[void](FillAndConnect $x $Host_ "$Port" $Code)
|
||||
$hud = ""
|
||||
if (-not (Wait-Live 14 ([ref]$hud))) { throw "never reached a live session" }
|
||||
Write-Host " HUD: $hud"
|
||||
if (-not $hud.Contains("v4")) { throw "V-B4 FAIL: HUD does not report protocol v4; got [$hud]" }
|
||||
Write-Host " PASS: live session on protocol v4"
|
||||
|
||||
$xml = Get-UiText
|
||||
$sb = SurfaceBounds $xml
|
||||
if (-not $sb) { throw "cannot locate SurfaceView bounds" }
|
||||
$vx1=$sb[0]; $vy1=$sb[1]; $vx2=$sb[2]; $vy2=$sb[3]
|
||||
$vcx=[int](($vx1+$vx2)/2); $vcy=[int](($vy1+$vy2)/2)
|
||||
Write-Host " surface bounds = [$vx1,$vy1][$vx2,$vy2] center=($vcx,$vcy)"
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-b1-live.png" | Out-Null
|
||||
|
||||
# =================================================================
|
||||
Write-Host "=== 2. V-B1a: relative mode moves PC pointer (swipe right) ==="
|
||||
# Park the PC cursor at a known point so the delta is unambiguous.
|
||||
# Let the link drain first (same discipline as 2c): residual events from an
|
||||
# earlier gesture would otherwise be attributed to this one.
|
||||
Start-Sleep -Milliseconds 1000
|
||||
[PC]::Set([int]($SW/2), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$before = [PC]::Get()
|
||||
# A horizontal swipe of ~40% of the view width. With sensitivity 1.0 and
|
||||
# view width mapping to desktop width, expected delta = 0.4 * SW.
|
||||
$swipeFromX = [int]($vx1 + ($vx2-$vx1)*0.30)
|
||||
$swipeToX = [int]($vx1 + ($vx2-$vx1)*0.70)
|
||||
[void](AdbShell @("input","swipe","$swipeFromX","$vcy","$swipeToX","$vcy","400"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$after = [PC]::Get()
|
||||
$moved = $after[0] - $before[0]
|
||||
Write-Host " swipe dx_device=$($swipeToX-$swipeFromX)px -> PC pointer dx=$moved px (before x=$($before[0]), after x=$($after[0]))"
|
||||
if ($moved -le 0) { throw "V-B1a FAIL: rightward swipe did not move the pointer right (dx=$moved)" }
|
||||
# Direction check is the hard judgement; exact magnitude depends on how many
|
||||
# ACTION_MOVE samples the ROM delivers for an `input swipe` (not equal to the
|
||||
# finger travel), so magnitude is reported, not asserted.
|
||||
Write-Host " PASS: relative mode moves the pointer in the finger's direction"
|
||||
|
||||
Write-Host "=== 2b. V-B1a negative: swipe left moves pointer left ==="
|
||||
Start-Sleep -Milliseconds 1000
|
||||
[PC]::Set([int]($SW/2), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$before2 = [PC]::Get()
|
||||
[void](AdbShell @("input","swipe","$swipeToX","$vcy","$swipeFromX","$vcy","400"))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$after2 = [PC]::Get()
|
||||
$moved2 = $after2[0] - $before2[0]
|
||||
Write-Host " PC pointer dx=$moved2 px (before x=$($before2[0]), after x=$($after2[0]))"
|
||||
if ($moved2 -ge 0) { throw "V-B1a FAIL: leftward swipe did not move the pointer left (dx=$moved2)" }
|
||||
Write-Host " PASS: negative direction correct (sign preserved)"
|
||||
|
||||
Write-Host "=== 2c. relative mode does NOT teleport the pointer on touch-down ==="
|
||||
# The defining property of relative mode: ACTION_DOWN must not send an
|
||||
# absolute move. Tap at the far LEFT of the view; the pointer must not jump
|
||||
# to the left edge of the desktop.
|
||||
#
|
||||
# HARNESS DISCIPLINE (measured 2026-10-31): a swipe's final relative events
|
||||
# can still be in flight when we re-park the cursor. The first attempt showed
|
||||
# phantom "jumps" of 15px and 298px that were really the PREVIOUS swipe's
|
||||
# queued deltas landing after our SetCursorPos. So: wait for the link to go
|
||||
# quiet, then require the baseline to be STABLE across two reads before
|
||||
# judging. Otherwise this test would report a product defect that isn't one.
|
||||
Start-Sleep -Milliseconds 1200
|
||||
[PC]::Set([int]($SW*0.8), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 500
|
||||
$stable = $false
|
||||
for ($i=0; $i -lt 6; $i++) {
|
||||
$r1 = [PC]::Get(); Start-Sleep -Milliseconds 350; $r2 = [PC]::Get()
|
||||
if ($r1[0] -eq $r2[0] -and $r1[1] -eq $r2[1]) { $stable = $true; break }
|
||||
Write-Host " cursor still settling: ($($r1[0]),$($r1[1])) -> ($($r2[0]),$($r2[1]))"
|
||||
}
|
||||
if (-not $stable) { throw "cannot obtain a stable cursor baseline (residual relative events never drained)" }
|
||||
$beforeTap = [PC]::Get()
|
||||
$tapX = [int]($vx1 + ($vx2-$vx1)*0.05)
|
||||
[void](AdbShell @("input","tap","$tapX","$vcy"))
|
||||
Start-Sleep -Milliseconds 700
|
||||
$afterTap = [PC]::Get()
|
||||
$jump = [Math]::Max([Math]::Abs($afterTap[0] - $beforeTap[0]), [Math]::Abs($afterTap[1] - $beforeTap[1]))
|
||||
$dbgTap = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
Write-Host " tap at view-left x=$tapX ; pointer ($($beforeTap[0]),$($beforeTap[1])) -> ($($afterTap[0]),$($afterTap[1])) jump=$jump px"
|
||||
if ($dbgTap.Contains($F_LONGPRESS)) { throw "tap was interpreted as a long press; harness timing is wrong" }
|
||||
if ($jump -gt 8) { throw "V-B1a FAIL: touch-down teleported the pointer by $jump px (absolute move leaked into relative mode)" }
|
||||
Write-Host " PASS: relative mode leaves the pointer where it was (no teleport)"
|
||||
|
||||
# =================================================================
|
||||
Write-Host "=== 3. V-B2: switch to ABSOLUTE mode, re-run R7 touch mapping ==="
|
||||
[void](Set-Mode "absolute")
|
||||
$hud2 = ""
|
||||
$m2 = [regex]::Match((Get-UiText),'text="([^"]*RTT=[^"]*)"')
|
||||
if ($m2.Success) { $hud2 = [System.Net.WebUtility]::HtmlDecode($m2.Groups[1].Value) }
|
||||
Write-Host " HUD: $hud2"
|
||||
if (-not $hud2.Contains($F_ABSOLUTE)) { throw "V-B2 FAIL: HUD still does not say absolute; got [$hud2]" }
|
||||
Write-Host " PASS: mode switched to absolute (button label and HUD agree)"
|
||||
|
||||
# R7 expectations for 1920x1080 (physical pixels, 125% scaling does NOT apply
|
||||
# to the injected desktop space - the agent injects into the physical desktop)
|
||||
$r7 = @(
|
||||
@{ frac=0.10; ex=[int](192); ey=[int](108) },
|
||||
@{ frac=0.90; ex=[int](1728); ey=[int](972) },
|
||||
@{ frac=0.50; ex=[int](960); ey=[int](540) }
|
||||
)
|
||||
$worst = 0
|
||||
foreach ($c in $r7) {
|
||||
$tx = [int]($vx1 + ($vx2-$vx1)*$c.frac)
|
||||
$ty = [int]($vy1 + ($vy2-$vy1)*$c.frac)
|
||||
[void](AdbShell @("input","tap","$tx","$ty"))
|
||||
Start-Sleep -Milliseconds 400
|
||||
$p = [PC]::Get()
|
||||
$errX = [Math]::Abs($p[0] - $c.ex)
|
||||
$errY = [Math]::Abs($p[1] - $c.ey)
|
||||
$e = [Math]::Max($errX,$errY)
|
||||
if ($e -gt $worst) { $worst = $e }
|
||||
Write-Host " frac=$($c.frac) expect=($($c.ex),$($c.ey)) actual=($($p[0]),$($p[1])) err=($errX,$errY)"
|
||||
}
|
||||
Write-Host " worst R7 error = $worst px"
|
||||
if ($worst -gt 8) { throw "V-B2 FAIL: R7 absolute mapping error $worst px > 8px" }
|
||||
Write-Host " PASS: absolute mode still maps 1:1 (R7 regression green)"
|
||||
|
||||
# =================================================================
|
||||
Write-Host "=== 4. V-B3: long press opens a context menu; drag does NOT ==="
|
||||
# Switch back to relative for the long-press test (that is the default mode
|
||||
# users get, and the gesture must work there).
|
||||
[void](Set-Mode "relative")
|
||||
|
||||
# Use a desktop of our own: the proof's WinForms target window (R-11: never
|
||||
# inject into a window we do not own - it would modify user data).
|
||||
$target = Join-Path $repo "tools\text_target.ps1"
|
||||
if (-not (Test-Path $target)) { throw "missing $target" }
|
||||
$tp = Start-Process powershell -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$target `
|
||||
-PassThru -WindowStyle Minimized
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host " target window started (pid $($tp.Id))"
|
||||
|
||||
# Park the pointer over the middle of the target window's edit area by
|
||||
# absolute injection first (so we know where the long press lands).
|
||||
[PC]::Set([int]($SW/2), [int]($SH/2))
|
||||
Start-Sleep -Milliseconds 300
|
||||
$preLong = [PC]::Get()
|
||||
$rcBefore = RightClickCount
|
||||
Write-Host " right-click counter before: $rcBefore"
|
||||
[void](AdbShell @("input","swipe","$vcx","$vcy","$vcx","$vcy","900")) # press & hold ~900ms, no movement
|
||||
Start-Sleep -Milliseconds 700
|
||||
$rcAfter = RightClickCount
|
||||
$debug = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
& powershell -NoProfile -ExecutionPolicy Bypass -File "$repo\tools\shot.ps1" -Out "$ev\v-b3-longpress.png" | Out-Null
|
||||
Write-Host " right-click counter after : $rcAfter (source tag in HUD)"
|
||||
Write-Host " screen after long press: $debug"
|
||||
if ($rcBefore -lt 0 -or $rcAfter -lt 0) { throw "V-B3 FAIL: could not read the right-click counter from the HUD" }
|
||||
if ($rcAfter -le $rcBefore) {
|
||||
throw "V-B3 FAIL: long press did not inject a right click (counter stayed $rcAfter; screen: $debug)"
|
||||
}
|
||||
Write-Host " PASS: long press injected a right click (counter $rcBefore -> $rcAfter)"
|
||||
|
||||
Write-Host "=== 4b. V-B3 negative: a DRAG must not fire long press ==="
|
||||
$rcBeforeDrag = RightClickCount
|
||||
[void](AdbShell @("input","swipe","$vcx","$vcy",$([int]($vcx+120)),$([int]($vcy+80)),900))
|
||||
Start-Sleep -Milliseconds 700
|
||||
$rcAfterDrag = RightClickCount
|
||||
$debug2 = ((VisibleTexts (Get-UiText)) -join " || ")
|
||||
Write-Host " right-click counter after drag: $rcAfterDrag (was $rcBeforeDrag)"
|
||||
Write-Host " screen after drag: $debug2"
|
||||
if ($rcAfterDrag -gt $rcBeforeDrag) {
|
||||
throw "V-B3 FAIL: drag injected a right click (counter $rcBeforeDrag -> $rcAfterDrag); long press must be suppressed once the finger moves"
|
||||
}
|
||||
Write-Host " PASS: drag did not fire long press"
|
||||
|
||||
Write-Host "=== 4c. V-B3: two-finger right click still works ==="
|
||||
$rcBeforeTwo = RightClickCount
|
||||
# adb cannot synthesize a two-finger tap (lessons R-6): this is a MANUAL item.
|
||||
Write-Host " (two-finger tap is not drivable via adb - recorded as manual, not asserted here)"
|
||||
|
||||
Stop-Process -Id $tp.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
finally {
|
||||
[void][PC]::Set($origCursor[0], $origCursor[1])
|
||||
[void](AdbShell @("svc","power","stayon","false"))
|
||||
if ($origIme -and $origIme -ne "null") {
|
||||
[void](AdbShell @("ime","enable",$origIme))
|
||||
[void](AdbShell @("ime","set",$origIme))
|
||||
Write-Host " IME restored: $(((AdbShell @('settings','get','secure','default_input_method')) -join '').Trim())"
|
||||
}
|
||||
Write-Host " PC cursor restored to ($($origCursor[0]),$($origCursor[1]))"
|
||||
}
|
||||
|
||||
Write-Host "=== REL MOUSE PHONE PROOF COMPLETE ==="
|
||||
Loading…
Reference in New Issue
Block a user