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:
郑博文 2026-10-31 17:35:18 +08:00
parent aede7b6c3c
commit 1a6c3f15a2
2 changed files with 525 additions and 6 deletions

View File

@ -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" }

View 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 ==="