Files
ykxmz/_tests/test_quickbar.py
T
xmz7891 1d368979aa 2.2.0:手表端连上后先到「远控 / 静默更新被控端」选择页
== 用户要求 ==

"安卓那个逻辑也要改一下,就是你输入配对码后,进入一个页面,
 页面有两个按钮,第一个按钮是远控,第二个按钮是静默更新被控端"

== 做成什么样 ==

新增 choicePanel,连接成功后先进这一页:

    已连接「电脑名」
    电脑端 2.1.5 → 可更新到 2.2.0

    [        远控        ]   <- 第一个按钮
    [   静默更新被控端    ]   <- 第二个按钮

按钮顺序按用户说的来(远控在前)。

页面顶上显示电脑名和**电脑端当前版本** —— 用户之前要求
"手表端连接那个电脑端也可以查看电脑端目前所处的版本",
这里是最自然的展示位置(不弹框,只显示一行)。

为什么不让它直接进控制页:老版本的被控端可能缺功能或有 bug,
"先更新再远控"是更合理的顺序 —— 但不强制,给用户自己选。

== 一个刻意不改的地方 ==

自动切到中转服务器**恢复**时的 `showControlPanel()` **没动** ——
用户本来就在远控页,中途弹一层选择页会打断他。
测试里专门断言了这一点,免得以后有人"顺手统一一下"改坏。

== 顺手修的几个问题 ==

1. **忘了定义 lastDeviceName** —— 写选择页时凭直觉用了这个变量,
   编译报"找不到符号"。补上字段并在连接回调里赋值。
   (教训:写代码时要用的变量,先确认它存在。)

2. **泄漏闸门拦下我自己** —— 这两天写的一批服务器诊断/配置脚本里
   硬编码了服务器 IP(26 处,5 个文件)。规则是 IP 不能进任何
   被跟踪的文件。已全部改成占位符,真实值仍从
   _build/.lightsail_host(已 gitignore)读。

3. **verify_all.ps1 删掉「锁屏时记 SKIP」的逻辑** ——
   用户明确要求删。那个好意带来一个更糟的后果:锁屏时几关根本不跑,
   回归"全绿"了但关键测试其实一跳而过。
   **"因为环境不满足所以不测"比"测了发现失败"更危险。**
   删掉之后果然暴露出 3 个 FAIL —— 屏幕解锁后重跑全部通过,
   确认是环境问题而不是代码问题。

== 服务器这边(不在这份提交里,但同一天做的)==

  * nginx 按用户要求重排:
      IP(80) 任何路径 -> 404(没东西)
      域名 / -> 官网;/zhongzhuanzhan -> 中转站设置
      9100 -> 同样的内容(等 AWS 开防火墙)
  * 删掉早年 Cloudflare Tunnel 的 systemd 残留
  * **补上 ACME 挑战例外** —— 这是最要紧的一条:
    80 端口原来把所有请求都 301 到 https,包括
    /.well-known/acme-challenge/,**续期时会失败**。
    而且它是延迟爆发的(最长 90 天后证书到期才暴露)。
  * 清理 1Panel 安装包残留(223MB)

== 验证 ==

    全量回归 21 关全部 PASS(零 FAIL,桌面可用状态)
    Java 编译通过(android-34, --release 8)
    623+ Python 文件语法通过
    泄漏闸门通过
    全部产物重建,Win7 兼容校验通过
2026-10-10 17:13:24 +08:00

263 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''验「手表快捷栏」和「允许新设备连接」。
== 验什么 ==
一、快捷栏的形状
* 一共 4 格,**每格都能自定义**
* 「显示桌面」是普通可选动作(默认放第 4 格,但能换掉)
* 8 个动作都在
* 挂进了 onCreate、加进了 showOnly 的数组
* 设置页里有入口
* 鼠标触摸板在
二、按钮大小
用户反馈原话:"屏幕最上面这个按钮有些按不到,按钮太小了,做大一点"
* 栏高做成常量(>= 44dp),而且**栏高 / 面板 margin / addView
三处用同一个常量** —— 分三处写的话改一个忘一个,
表现是"内容被盖住"或"上面空一条"
三、电脑端 allow_new_pairing 这个 feature
== 这条断言改过一次,值得记下来 ==
用户最早说:"最多可以加 4 个,其中有一个按钮不能改,
功能就是一键返回桌面"。
第一版就按"第 4 格写死 desktop"实现,也按那个断言测。
后来用户看到实物改了主意:"第一个第二个第三个第四个都能换东西"。
所以现在验的是**相反**的事。教训:
**别把"用户当时的一句话"当成永久约束焊进结构里**;
真是约束的话,注释里要写清是什么时候的要求,
下次他改主意时才知道该动哪儿。
'''
from __future__ import annotations
import re
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "windows"))
FAILS: list = []
def check(name: str, ok: bool, why: str = "") -> None:
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name,
(" —— " + why) if why else ""))
if not ok:
FAILS.append(name)
def main() -> int:
print("=" * 58)
print(" 手表快捷栏 + 允许新设备连接")
print("=" * 58)
print()
main_java = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "MainActivity.java").read_text(encoding="utf-8")
# ---------------- 一、快捷栏 ----------------
print("一、快捷栏(手表端)")
for token, label in [
("private void buildQuickBar()", "有 buildQuickBar"),
("private void refreshQuickBar()", "有 refreshQuickBar"),
("private void onQuickClick(int slot)", "有点击分发 onQuickClick"),
("private void runQuickAction(String id)", "有动作执行 runQuickAction"),
("private void loadQuickSlots()", "有读取设置"),
("private void saveQuickSlots()", "有保存设置"),
]:
check(label, token in main_java)
# 4 格,全部可自定义。这条断言改过一次,见文件头。
check("★ 一共 4 格", "QUICK_SLOTS = 4" in main_java, "用户要求 4 格")
check("★ 旧的 3+1 分法已经不在了",
"QUICK_CUSTOM_SLOTS" not in main_java, "还有残留说明没改干净")
check("★ 没有把 desktop 写死在某格",
': "desktop";' not in main_java,
"还有地方把 desktop 写死成某一格")
check("★ desktop 在可选动作里(能换到任意一格)",
bool(re.search(r'\{"desktop",\s*"显示桌面"\}', main_java)),
"不在 QUICK_ACTIONS 里的话用户没法把它放到别的格")
check("四格平等:都从 quickSlots 取",
"String id = quickSlots[i];" in main_java
and "String id = quickSlots[slot];" in main_java,
"还有特殊分支")
check("默认值就是那 4 个",
'new String[]{"file", "mirror", "sfx", "desktop"}' in main_java)
check("desktop 走的是 show_desktop 命令",
'"cmd", "show_desktop"' in main_java)
print()
print(" 8 个可选动作:")
for aid, label in [("file", "传文件"), ("mirror", "屏幕镜像"),
("mouse", "鼠标控制"), ("sfx", "音效"),
("power", "关机"), ("tts", "语音播报"),
("image", "投图到电脑"), ("desktop", "显示桌面")]:
check(" %s(%s)" % (aid, label),
('{"%s", "%s"}' % (aid, label)) in main_java)
# ---------------- 二、按钮大小 ----------------
print()
print("二、按钮大小(用户反馈「太小了、按不到」)")
m = re.search(r"QUICK_BAR_DP = (\d+)", main_java)
check("★ 栏高做成常量 QUICK_BAR_DP", m is not None,
"没有常量的话栏高和面板 margin 会各写各的")
h = int(m.group(1)) if m else 0
check("★ 栏高 >= 44dp(接近 Android 建议的可点区域 48)", h >= 44,
"现在是 %d" % h)
check("★ 面板 topMargin 用同一个常量",
"flp.topMargin = dp(QUICK_BAR_DP);" in main_java,
"分两处写的话改一个忘一个")
check("★ addView 也用同一个常量",
"MATCH_PARENT, dp(QUICK_BAR_DP)));" in main_java)
check("★ 按钮字号 >= 13(原来是 11)",
"b.setTextSize(14f);" in main_java)
# ⚠ 只查**代码**,不查注释。
# 第一版这里是 `"dp(30)" not in main_java`,结果被
# applyPanelTopMargin 的 javadoc 里那句旧说明误报了 ——
# 注释里提一下旧值是很正常的事("以前是 30dp"),
# 不该因为它变红。
code_lines = [ln for ln in main_java.splitlines()
if not ln.strip().startswith(("*", "//", "/*"))]
hard = [ln.strip() for ln in code_lines if "dp(30)" in ln]
check("没有残留的 dp(30)(旧栏高)", not hard,
"还有代码用旧的 30dp:%s" % hard)
# ---------------- 三、挂接 ----------------
print()
print("三、挂接(漏了就白写)")
i0 = main_java.find("protected void onCreate")
oncreate = main_java[i0:i0 + 7000]
check("★ onCreate 里调了 buildQuickBar()", "buildQuickBar();" in oncreate,
"没挂上去栏根本不显示")
check("★ onCreate 里调了 loadQuickSlots()", "loadQuickSlots();" in oncreate)
check("★ 快捷栏最后 addView(保证在最上层)",
oncreate.find("root.addView(quickBar")
> oncreate.find("root.addView(mousePanel"),
"加在面板之前会被面板整个盖住")
# ⚠ 断言写成"数组里有这几个面板"而不是"精确等于某个字符串" ——
# 第一版写的是 `"settingsPanel, mousePanel};" in main_java`,
# 后来往数组里加了 choicePanel,那串就不匹配了(假失败)。
# 验"包含"而不是"全等",才不会因为多一个面板就红。
m_arr = re.search(r"View\[\] all = \{([^}]*)\}", main_java)
arr = m_arr.group(1) if m_arr else ""
for panel in ("connectPanel", "controlPanel", "mirrorPanel", "lanPanel",
"settingsPanel", "mousePanel", "choicePanel"):
check("★ showOnly 数组里有 %s" % panel, panel in arr,
"漏了的话那个面板永远不隐藏")
check("有 applyPanelTopMargin(面板给栏让位)",
"private void applyPanelTopMargin" in main_java)
# ---------------- 四、设置页 ----------------
print()
print("四、设置页入口(没入口用户改不了)")
check("有 fillSettingsExtra",
"private void fillSettingsExtra()" in main_java)
check("有 pickQuickSlot(选动作)",
"private void pickQuickSlot(final int slot)" in main_java)
check("设置页里加了「快捷栏」小节",
'sectionTitle("快捷栏")' in main_java)
check("设置页里加了「允许新设备连接」小节",
'sectionTitle("允许新设备连接")' in main_java)
check("界面上说明了四格都能换", "每一个都能换" in main_java,
"不说明的话用户不知道第四格也能改")
# ---------------- 五、鼠标触摸板 ----------------
print()
print("五、鼠标触摸板")
check("有 buildMousePanel", "private void buildMousePanel()" in main_java)
check("用 GestureDetector(单击/双击/拖动)",
"GestureDetector" in main_java
and "onSingleTapConfirmed" in main_java
and "onDoubleTap" in main_java
and "onScroll" in main_java)
check("单击 -> 左键", 'sendMouse("click", "left", 0)' in main_java)
check("双击 -> 右键", 'sendMouse("click", "right", 0)' in main_java)
check("滚轮上/下", 'sendMouse("scroll", "", 3)' in main_java
and 'sendMouse("scroll", "", -3)' in main_java)
check("拖动 -> moveby", '"moveby"' in main_java)
check("isConnected() 存在(第一版漏了,编译才发现)",
"private boolean isConnected()" in main_java)
# ---------------- 五之二、连接后的选择页 ----------------
print()
print("五之二、连接后的选择页(远控 / 静默更新被控端)")
#
# 用户要求:"输入配对码后,进入一个页面,页面有两个按钮,
# 第一个按钮是远控,第二个按钮是静默更新被控端"
check("有 choicePanel 字段", "private FrameLayout choicePanel;" in main_java)
check("有 buildChoicePanel", "private void buildChoicePanel()" in main_java)
check("有 showChoicePanel", "private void showChoicePanel()" in main_java)
check("★ 第一个按钮是「远控」", 'bigButton("远控"' in main_java,
"按钮顺序要跟用户说的一致")
check("★ 第二个按钮是「静默更新被控端」",
'bigButton("静默更新被控端"' in main_java)
check(" 更新按钮复用 checkPcUpdate()", "checkPcUpdate();" in main_java)
check("★ 连接成功后进的是选择页(不是直接进控制页)",
"showChoicePanel();" in main_java,
"改漏了的话用户根本看不到选择页")
check(" 选择页也做了顶部让位",
"applyPanelTopMargin(choicePanel);" in main_java)
check(" 选择页显示电脑端版本", "choiceVersionText" in main_java)
check(" 记下了电脑名(不然只能显示 IP)",
"private volatile String lastDeviceName" in main_java,
"第一版忘了定义这个字段,编译报「找不到符号」")
# ⚠ 自动切中转恢复那个调用点**不该**改成选择页 ——
# 用户本来就在远控页,中途弹选择页会打断他。
i = main_java.find("已切到中转服务器,连接恢复")
check("★ 自动切中转恢复后仍是控制页(没被误改)",
i > 0 and "showControlPanel();" in main_java[i:i + 300],
"改错地方的话,用着用着会突然被弹回选择页")
# ---------------- 六、电脑端 allow_new_pairing ----------------
print()
print("六、电脑端 allow_new_pairing feature")
from lanremote.config import Store
from lanremote.features import Features
class FakeActions:
"""Features 要的是 actions 对象,不是 store(第一版就是这里传错了)。"""
def __init__(self, store):
self.store = store
self.ui = None
st = Store(Path(tempfile.mkdtemp()))
feats = Features(FakeActions(st))
check("能查到当前状态", "允许" in feats.run("allow_new_pairing", {}))
out = feats.run("allow_new_pairing", {"allow": False})
check("能关掉", "禁止" in out, "拿到 %r" % out)
check(" ★ 存储里真的变了", st.get("allow_new_pairing") is False,
"拿到 %r" % st.get("allow_new_pairing"))
check(" 关掉之后再查也是关",
"不允许" in feats.run("allow_new_pairing", {}))
out = feats.run("allow_new_pairing", {"allow": True})
check("能打开", "已允许" in out, "拿到 %r" % out)
check(" 存储里也变了", st.get("allow_new_pairing") is True)
try:
feats.run("allow_new_pairing_typo", {})
check("不认识的名字会报错", False, "它竟然没报错")
except ValueError:
check("不认识的名字会报错", True)
print()
print("=" * 58)
if FAILS:
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
return 1
print(" 全部通过")
return 0
if __name__ == "__main__":
sys.exit(main())