test(android): 相对鼠标/长按右键纯逻辑单测 17 例
覆盖 V-B1 判据对应的行为,不是「跑通就算」: - 慢速小幅不丢位移(10 次 0.3px 必须发出 3px) - 负值向远离零取整(-0.7 取 -1,向零截断会让反向慢滑丢位移) - 撞边不积债(回滑立即发出,不因补偿而空转) - i16 越界 clamp 而非回绕 - 长按三条判据:够久、没动、非双指 已做 mutation 反向验证:把期望 3 改成 4 后测试确实红,改回后绿(断言非空转)。
This commit is contained in:
parent
7c3b6b3e93
commit
aede7b6c3c
@ -0,0 +1,172 @@
|
||||
package com.verify.remote
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 3b/V-B1:相对鼠标的纯逻辑(协议 v4)。
|
||||
*
|
||||
* 这些断言对应验收判据,不是「跑通就算」:
|
||||
* - V-B1b「慢速小幅滑动不丢位移」→ [accumulatorKeepsSubPixelRemainders]
|
||||
* - V-B1c「撞边后回滑立即响应」→ 累积器只做取整、不做补偿([accumulatorDoesNotBankLostSteps])
|
||||
*/
|
||||
class RelPointerTest {
|
||||
|
||||
// ---- 换算:视图像素 → 桌面像素 ----
|
||||
|
||||
@Test
|
||||
fun sensitivity_one_maps_full_view_width_to_full_desktop_width() {
|
||||
// Q4=A 的语义定义:1.0 = 手指扫过视图全宽 = 指针走完桌面全宽
|
||||
val d = RelPointer.toDesktopDelta(
|
||||
viewDeltaPx = 1080f, // 视图全宽(1080x607 letterbox)
|
||||
viewExtentPx = 1080,
|
||||
remoteExtentPx = 1920, // 远端桌面宽
|
||||
sensitivity = 1.0f,
|
||||
)
|
||||
assertEquals(1920f, d, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sensitivity_scales_linearly() {
|
||||
val base = RelPointer.toDesktopDelta(100f, 1080, 1920, 1.0f)
|
||||
val twice = RelPointer.toDesktopDelta(100f, 1080, 1920, 2.0f)
|
||||
assertEquals(base * 2f, twice, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun negative_delta_stays_negative() {
|
||||
// 向左滑必须得到负增量(绝对坐标是无符号的,相对通道是唯一有符号路径)
|
||||
val d = RelPointer.toDesktopDelta(-54f, 1080, 1920, 1.0f)
|
||||
assertTrue("向左滑应得到负增量,实际 $d", d < 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalid_extents_yield_zero_not_garbage() {
|
||||
// 视图未测量 / 远端尺寸未取到时必须不动,而不是按错误比例乱飞
|
||||
assertEquals(0f, RelPointer.toDesktopDelta(50f, 0, 1920, 1.0f), 0f)
|
||||
assertEquals(0f, RelPointer.toDesktopDelta(50f, 1080, 0, 1.0f), 0f)
|
||||
assertEquals(0f, RelPointer.toDesktopDelta(0f, 1080, 1920, 1.0f), 0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sensitivity_is_clamped_to_documented_range() {
|
||||
assertEquals(RelPointer.MIN_SENSITIVITY, RelPointer.clampSensitivity(0.01f), 0f)
|
||||
assertEquals(RelPointer.MAX_SENSITIVITY, RelPointer.clampSensitivity(99f), 0f)
|
||||
assertEquals(1.5f, RelPointer.clampSensitivity(1.5f), 0f)
|
||||
// 边界常量本身必须与方案 Q4=A 一致
|
||||
assertEquals(0.25f, RelPointer.MIN_SENSITIVITY, 0f)
|
||||
assertEquals(3.0f, RelPointer.MAX_SENSITIVITY, 0f)
|
||||
assertEquals(1.0f, RelPointer.DEFAULT_SENSITIVITY, 0f)
|
||||
}
|
||||
|
||||
// ---- 累积取整:V-B1b ----
|
||||
|
||||
@Test
|
||||
fun accumulatorKeepsSubPixelRemainders() {
|
||||
// 每次只移动 0.3 桌面像素:逐次 toInt() 会全被吞掉(位移静默丢失)。
|
||||
// 累积器必须在攒够 1 像素时发出,且总量守恒。
|
||||
val acc = RelAccumulator()
|
||||
var total = 0
|
||||
repeat(10) {
|
||||
acc.take(0.3f, 0f)?.let { total += it.first }
|
||||
}
|
||||
// 10 * 0.3 = 3.0 → 必须发出 3 像素,而不是 0
|
||||
assertEquals("慢速滑动不得丢位移", 3, total)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorDoesNotEmitBelowHalfPixel() {
|
||||
// 小于 0.5 的余量四舍五入后是 0 → 不发(否则会产生大量零位移消息)。
|
||||
// 注意阈值是 0.5 而不是 1.0:累积器用 roundToInt 而非 toInt,
|
||||
// 0.8 会先发出 1 并把 -0.2 留作余数(自我修正,长期总量守恒)。
|
||||
val acc = RelAccumulator()
|
||||
assertNull("0.4 四舍五入为 0,不得发送", acc.take(0.4f, 0.4f))
|
||||
// 再攒 0.4 → 合计 0.8,四舍五入为 1,此时必须发出
|
||||
val d = acc.take(0.4f, 0.4f)
|
||||
assertEquals(1, d!!.first)
|
||||
assertEquals(1, d.second)
|
||||
// 余数 -0.2:紧接着的 0.2 会被抵消成 0,不再发送(证明余数被正确保留)
|
||||
assertNull(acc.take(0.2f, 0.2f))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorRoundsNegativeValuesAwayFromZero() {
|
||||
// -0.7 应取 -1(向零截断会得到 0,反向慢滑就丢了)
|
||||
val acc = RelAccumulator()
|
||||
val d = acc.take(-0.7f, 0f)
|
||||
assertEquals(-1, d!!.first)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorAxesAreIndependent() {
|
||||
// 斜向慢滑:X 攒够时 Y 还没攒够,不能因为 Y 不够就把 X 也拖住
|
||||
val acc = RelAccumulator()
|
||||
val d = acc.take(1.0f, 0.2f)
|
||||
assertEquals(1, d!!.first)
|
||||
assertEquals(0, d.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorResetDropsRemainder() {
|
||||
// 手势/会话结束时清空:上一段的余数不该漏进下一段
|
||||
val acc = RelAccumulator()
|
||||
acc.take(0.6f, 0.6f)
|
||||
acc.reset()
|
||||
assertNull(acc.take(0.3f, 0.3f))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorDoesNotBankLostSteps() {
|
||||
// V-B1c:撞边时被 clamp 丢掉的位移**不回补**。累积器只负责取整,
|
||||
// 不负责补偿——否则撞边后回滑会先"空转"一段,违反「立即响应」。
|
||||
val acc = RelAccumulator()
|
||||
acc.take(1.0f, 0f) // 发出 +1
|
||||
val back = acc.take(-1.0f, 0f)
|
||||
assertEquals("回滑必须立即发出,不得因补偿而空转", -1, back!!.first)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accumulatorClampsToProtocolRange() {
|
||||
// 协议字段是 i16;越界必须被 clamp 而不是回绕(回绕会让指针跳到屏幕另一侧)
|
||||
val acc = RelAccumulator()
|
||||
val d = acc.take(100_000f, -100_000f)
|
||||
assertEquals(32767, d!!.first)
|
||||
assertEquals(-32768, d.second)
|
||||
}
|
||||
|
||||
// ---- 长按右键:V-B3 ----
|
||||
|
||||
@Test
|
||||
fun longPress_fires_at_threshold_when_still() {
|
||||
assertTrue(LongPressPolicy.shouldFire(500L, 0f, 30f, false))
|
||||
assertTrue(LongPressPolicy.shouldFire(900L, 2f, 30f, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longPress_does_not_fire_too_early() {
|
||||
assertFalse(LongPressPolicy.shouldFire(499L, 0f, 30f, false))
|
||||
assertFalse(LongPressPolicy.shouldFire(0L, 0f, 30f, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longPress_does_not_fire_after_moving() {
|
||||
// V-B3「拖动窗口时不弹菜单」:超过 slop 一律不算长按
|
||||
assertFalse(LongPressPolicy.shouldFire(800L, 31f, 30f, false))
|
||||
assertFalse(LongPressPolicy.shouldFire(800L, 200f, 30f, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longPress_does_not_fire_with_two_fingers() {
|
||||
// 双指有自己的右键路径,长按不能叠加(否则一次双指轻点会出两个右键)
|
||||
assertFalse(LongPressPolicy.shouldFire(900L, 0f, 30f, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun long_press_threshold_is_pinned_to_decision() {
|
||||
// Q5=A:500ms,改动必须显式经过这条断言
|
||||
assertEquals(500L, LongPressPolicy.LONG_PRESS_MS)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user