「长按清空后仍停在 ACT_LOCK」这条断言测不出清空:进入该段前只输了 "135" (3 位),而确认在 <4 位时本就 disabled,即使长按没清空也照样停在锁屏 → 断言恒真,与措辞不符。 删掉该断言与其前的空点确认;清空的真正判据仍是紧接着的「输 1357 → 确认 → 解锁」:若没清空,缓冲会是 "135"+"1357" = 7 位错码,必 FAIL。已用变异验证: 临时去掉 lock_clear_all() 后该行确实 FAIL,恢复后 PASS。
356 lines
17 KiB
C
356 lines
17 KiB
C
/* test_settings_lock_host.c — 设置页/锁屏与 SDK 骨架的宿主集成测试(可选,需 SDK 源码树)。
|
||
*
|
||
* 目的:钉住 M3 骨架评审点名的两条坑(M5 桩注释①/②),这两条只有把真实 SDK
|
||
* view/activity/widget 链起来才能验证:
|
||
* ① 非根页控件必须在 on_enter 建(此时 activity 栈顶=本页)——否则 owner 绑到
|
||
* ACT_LIBRARY,跨页命中泄漏;
|
||
* ② 锁屏必须在自身 view 的 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c
|
||
* 的 on_swipe),否则左缘返回会把 ACT_LOCK page_pop 掉、PIN 被绕过。
|
||
* 另外覆盖:设置页回到书库(topbar 返回)、控件跨进出复用不重建、阅读方向经
|
||
* mr_reader_set_direction 切换、lock_view_should_show() 随 PIN 状态变化、
|
||
* 主题模式切换后整帧重绘不崩,以及两条端到端路径:
|
||
* - **锁屏页内自绘 12 键数字键盘**(1-9 / 退格·0·确认):错 PIN 留在锁屏 → 对 PIN
|
||
* page_pop 回书库;位数不足确认不可用、超长输入截断、退格/长按清空、错误后清空重输;
|
||
* 键高/间距/屏底 28px 热区几何契约;长按退格走真实 SDK opt-in 路径(未注册回调不产生
|
||
* MEOW_EV_LONG_PRESS = M29 评审 P2 的根因,注册后才清空);
|
||
* - 设置页两遍输入开启 PIN(仍走真实 MEOW_IME_NUM 键盘注入)。
|
||
*
|
||
* 编译:见 test/run_tests.sh —— 它把本测试、settings/lock/pin 源码与 SDK 源码
|
||
* (-DMEOW_UI_HOST_TEST,附 test_host_stub.c 打桩 font/global)一起编;SDK 对象清单在脚本里。
|
||
* 只发布 meow_ui.a(无 SDK 源码)的交付环境,run_tests.sh 自动 SKIP 本测试。
|
||
*/
|
||
#include "tuku_internal.h"
|
||
#include "logic/pin.h"
|
||
#include "meow_ui_internal.h" /* meow_widget_owner(宿主测试专用内部头) */
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <pthread.h>
|
||
|
||
static int g_fail;
|
||
#define CHECK(cond, ...) do { \
|
||
if (cond) { printf(" PASS\n"); } \
|
||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||
} while (0)
|
||
|
||
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||
void mr_nav_report(void) {}
|
||
|
||
static int g_lib_enters;
|
||
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
|
||
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
|
||
|
||
/* 清掉本次测试写入的三个小配置并删临时目录(meow_config 每键一文件)。 */
|
||
static void cleanup_cfg(const char *dir)
|
||
{
|
||
static const char *keys[3] = { "pin.enabled.cfg", "pin.hash.cfg", "pin.salt.cfg" };
|
||
char p[512];
|
||
for (int i = 0; i < 3; i++) {
|
||
snprintf(p, sizeof p, "%s/%s", dir, keys[i]);
|
||
unlink(p);
|
||
}
|
||
rmdir(dir);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 数字键盘注入(宿主没有真实触摸):按 MEOW_IME_NUM 键位投递 TAP,关闭键盘后由被测代码校验。
|
||
* NUM 布局(sdk/meow_ui_ime.c build_kb):KB_TOP=420;首行 470 是符号行 "-/.:*#+=",
|
||
* 数字九键行为 518/566/614(x0=70/190/310,键宽 100),底行(ESC|0|退格+OK)为 662;
|
||
* 点 y<420 的背景=关闭键盘。 */
|
||
/* ============================================================ */
|
||
typedef struct { const char *digits; int passes; } pin_job;
|
||
|
||
static void key_tap(char d)
|
||
{
|
||
int row, col;
|
||
if (d == '0') { row = 3; col = 1; }
|
||
else {
|
||
int v = d - '1';
|
||
if (v < 0 || v > 8) return;
|
||
row = v / 3;
|
||
col = v % 3;
|
||
}
|
||
/* 数字行 y=518/566/614(首行 470 是符号行 "-/.:*#+="),底行 662;列心 120/240/360。 */
|
||
meow_input_inject(MEOW_EV_TAP, 70 + col * 120 + 50, 518 + row * 48 + 18);
|
||
}
|
||
|
||
static void *pin_injector(void *arg)
|
||
{
|
||
pin_job *job = (pin_job *)arg;
|
||
usleep(150000); /* 等 text_input 开键盘并 drain 完残留事件 */
|
||
for (int p = 0; p < job->passes; p++) {
|
||
if (p) usleep(300000); /* 两遍输入之间等第二遍开键盘 */
|
||
for (const char *s = job->digits; *s; s++) {
|
||
key_tap(*s);
|
||
usleep(50000);
|
||
}
|
||
usleep(50000);
|
||
meow_input_inject(MEOW_EV_TAP, 240, 100); /* y<420:关闭键盘(非空→校验) */
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 锁屏页内自绘 12 键数字键盘(宿主没有真实触摸):按 widget 矩形中心投递 TAP。
|
||
* 键下标 = lock.c 创建序:0..8 = 1..9,9 = 退格,10 = 0,11 = 确认(LK_K*)。
|
||
* ============================================================ */
|
||
enum { LK_K1 = 0, LK_K9 = 8, LK_KBACK = 9, LK_K0 = 10, LK_KOK = 11 };
|
||
|
||
static int lock_key_center(meow_view *lock, int idx, int *cx, int *cy)
|
||
{
|
||
int r[4] = { 0 };
|
||
if (!lock || idx < 0 || idx >= lock->nwidgets || !lock->widgets[idx]) return 0;
|
||
meow_widget_rect(lock->widgets[idx], r);
|
||
*cx = r[0] + r[2] / 2;
|
||
*cy = r[1] + r[3] / 2;
|
||
return 1;
|
||
}
|
||
|
||
static void lock_tap_key(meow_view *lock, int idx)
|
||
{
|
||
int cx, cy;
|
||
if (lock_key_center(lock, idx, &cx, &cy))
|
||
meow_view_dispatch_event(MEOW_EV_TAP, cx, cy);
|
||
}
|
||
|
||
static int lock_key_idx(char d)
|
||
{
|
||
return (d == '0') ? LK_K0 : (d - '1');
|
||
}
|
||
|
||
static void lock_type(meow_view *lock, const char *digits)
|
||
{
|
||
for (const char *s = digits; *s; s++) lock_tap_key(lock, lock_key_idx(*s));
|
||
}
|
||
|
||
/* app.c 注册的 no-op 长按回调(事件先经 view 分发,锁屏页消费;其它页落到这里)。 */
|
||
static void app_long_press_noop(int x, int y) { (void)x; (void)y; }
|
||
|
||
static void settings_tap_pin_row(void){
|
||
/* 布局:sb + 56 顶栏 + 6 + 段头34 + 方向行64 + 12 + 段头34 → PIN 列表首行中心 +32 */
|
||
int y = meow_ui_statusbar_height() + 56 + 6 + 34 + 64 + 12 + 34 + 32;
|
||
pin_job job = { "2468", 2 };
|
||
pthread_t th;
|
||
if (pthread_create(&th, NULL, pin_injector, &job) != 0) return;
|
||
meow_view_dispatch_event(MEOW_EV_TAP, 200, y);
|
||
pthread_join(th, NULL);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
const char *base = getenv("TMPDIR");
|
||
if (!base || !base[0]) base = ".";
|
||
char tmpl[512];
|
||
snprintf(tmpl, sizeof tmpl, "%s/mr_settings_host_XXXXXX", base);
|
||
char *data = mkdtemp(tmpl);
|
||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||
setenv("MEOW_DATA_DIR", data, 1);
|
||
|
||
meow_ui_opts o = { 480, 720 };
|
||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||
|
||
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
|
||
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||
meow_view *settings = settings_view_create();
|
||
meow_view *lock = lock_view_create();
|
||
int sb = meow_ui_statusbar_height();
|
||
|
||
meow_view_page_push(ACT_LIBRARY);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "start at library");
|
||
|
||
/* ---- 设置页:on_enter 建控件 + owner 归属 ---- */
|
||
meow_view_page_push(ACT_SETTINGS);
|
||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active");
|
||
CHECK(settings->nwidgets == 5, "settings built 5 widgets (got %d)", settings->nwidgets);
|
||
int owner_ok = 1;
|
||
for (int i = 0; i < settings->nwidgets; i++)
|
||
if (meow_widget_owner(settings->widgets[i]) != ACT_SETTINGS) owner_ok = 0;
|
||
CHECK(owner_ok, "settings widgets owned by ACT_SETTINGS (no cross-page leak)");
|
||
|
||
/* 布局几何:顶栏贴状态栏、三组列表不越界、页脚在屏内。 */
|
||
int r_tb[4] = { 0 }, r_about[4] = { 0 };
|
||
meow_widget_rect(settings->widgets[0], r_tb);
|
||
meow_widget_rect(settings->widgets[3], r_about);
|
||
CHECK(r_tb[1] == sb && r_tb[3] == 56, "topappbar sits below statusbar");
|
||
CHECK(r_about[1] + r_about[3] < meow_ui_height() - 60,
|
||
"about list leaves room for footer (bottom=%d)", r_about[1] + r_about[3]);
|
||
printf(" geometry: topbar y=%d, about list y=%d h=%d (screen %dx%d)\n",
|
||
r_tb[1], r_about[1], r_about[3], meow_ui_width(), meow_ui_height());
|
||
|
||
/* 阅读方向行:段头 34 + 行中心。 */
|
||
int dir_y = sb + 56 + 6 + 34 + 32;
|
||
int d0 = mr_reader_get_direction();
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 120, dir_y) == 1,
|
||
"tap on direction row consumed by settings on_event");
|
||
CHECK(mr_reader_get_direction() != d0,
|
||
"direction toggled through mr_reader_set_direction");
|
||
|
||
/* topbar 返回 → page_pop 回书库 */
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 22, sb + 28) == 1, "back tap consumed");
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back popped to library");
|
||
CHECK(g_lib_enters == 2, "library re-entered after pop (enters=%d)", g_lib_enters);
|
||
|
||
/* 再次进入:控件复用(不在 on_enter 重复创建/泄漏) */
|
||
meow_view_page_push(ACT_SETTINGS);
|
||
CHECK(settings->nwidgets == 5, "settings widgets reused (got %d)", settings->nwidgets);
|
||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active again");
|
||
|
||
/* 设置页不消费 SWIPE:交给 app.c 的 page_pop */
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 200) == 0,
|
||
"settings leaves SWIPE to app.c (back = pop)");
|
||
meow_view_page_pop(); /* 手动模拟 app.c 的返回行为,回到书库 */
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "manual pop back to library");
|
||
|
||
/* ---- 锁屏:PIN 状态驱动 + SWIPE 拦截(坑②) ---- */
|
||
mr_pin_disable();
|
||
CHECK(lock_view_should_show() == 0, "lock hidden when PIN disabled");
|
||
CHECK(mr_pin_set("1357") == MR_PIN_OK, "enable PIN");
|
||
CHECK(lock_view_should_show() == 1, "lock shown when PIN enabled");
|
||
|
||
meow_view_page_push(ACT_LOCK);
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "lock active");
|
||
CHECK(lock->nwidgets == 12, "lock built 12-key pad on enter (got %d)", lock->nwidgets);
|
||
int lock_owner_ok = 1;
|
||
for (int i = 0; i < lock->nwidgets; i++)
|
||
if (meow_widget_owner(lock->widgets[i]) != ACT_LOCK) lock_owner_ok = 0;
|
||
CHECK(lock_owner_ok, "pad keys owned by ACT_LOCK (no cross-page leak)");
|
||
|
||
/* 几何契约:键 ≥64px 高、间距 ≥8px、不覆盖屏底 28px 上滑热区、全在屏内。 */
|
||
int rk[12][4];
|
||
for (int i = 0; i < 12; i++) meow_widget_rect(lock->widgets[i], rk[i]);
|
||
int geo_ok = 1, gap_ok = 1, hot_ok = 1;
|
||
for (int i = 0; i < 12; i++) {
|
||
if (rk[i][2] < 64 || rk[i][3] < 64) geo_ok = 0;
|
||
if (rk[i][0] < 0 || rk[i][0] + rk[i][2] > meow_ui_width()) geo_ok = 0;
|
||
if (rk[i][1] + rk[i][3] > meow_ui_height() - 28) hot_ok = 0;
|
||
}
|
||
/* 列间距(行内 0/1/2)与行间距(行 0 与行 3:下标 0 与 9)。 */
|
||
if (rk[1][0] - (rk[0][0] + rk[0][2]) < 8) gap_ok = 0;
|
||
if (rk[2][0] - (rk[1][0] + rk[1][2]) < 8) gap_ok = 0;
|
||
if (rk[9][1] - (rk[0][1] + rk[0][3]) < 8) gap_ok = 0;
|
||
CHECK(geo_ok, "pad keys >=64px and within screen");
|
||
CHECK(gap_ok, "pad gaps >=8px");
|
||
CHECK(hot_ok, "pad leaves the 28px bottom swipe hot zone");
|
||
printf(" pad geometry: key %dx%d, pad top=%d bottom=%d (screen %dx%d)\n",
|
||
rk[0][2], rk[0][3], rk[0][1], rk[11][1] + rk[11][3],
|
||
meow_ui_width(), meow_ui_height());
|
||
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 240) == 1,
|
||
"lock consumes SWIPE (left-edge back cannot bypass PIN)");
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after swipe");
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 5, 400) == 1, "lock consumes stray taps");
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "still locked after stray tap");
|
||
|
||
CHECK(mr_pin_verify("1357") == 1, "stored PIN verifies");
|
||
CHECK(mr_pin_verify("1358") == 0, "wrong PIN rejected");
|
||
|
||
/* 空输入 / 位数不足:确认不可用(不误判、不退出锁屏)。 */
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "empty confirm keeps lock");
|
||
lock_type(lock, "135");
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "confirm disabled below 4 digits");
|
||
|
||
/* 第 4 位后确认可用:1-3-5-7 → 回书库。 */
|
||
lock_tap_key(lock, lock_key_idx('7'));
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "4-digit correct PIN pops lock to library");
|
||
CHECK(meow_ime_active() == 0, "in-page pad never opens the system IME");
|
||
|
||
/* 错 PIN:留在锁屏 + 清空;再输对 → 解锁(证明错误后缓冲已清空)。 */
|
||
meow_view_page_push(ACT_LOCK);
|
||
lock_type(lock, "1358");
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)");
|
||
lock_type(lock, "1357");
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
|
||
"retry after wrong PIN unlocks (buffer cleared on error)");
|
||
|
||
/* 8 位上限:9 个键只收前 8 位;退格 4 次回到 "1357" → 解锁。 */
|
||
meow_view_page_push(ACT_LOCK);
|
||
lock_type(lock, "135713579");
|
||
for (int i = 0; i < 4; i++) lock_tap_key(lock, LK_KBACK);
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
|
||
"9th digit ignored (max 8) + backspace removes digits");
|
||
|
||
/* 退格空缓冲边界:多按不崩,随后正常输入。 */
|
||
meow_view_page_push(ACT_LOCK);
|
||
for (int i = 0; i < 3; i++) lock_tap_key(lock, LK_KBACK);
|
||
lock_type(lock, "1357");
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "backspace on empty buffer is a no-op");
|
||
|
||
/* 长按退格清空(M29 评审 P2-1)走真实 SDK 路径:
|
||
* SDK 长按是 opt-in —— 未注册回调时 meow_input_long_press_poll 恒返回 0,
|
||
* MEOW_EV_LONG_PRESS 永远不会产生(真机上「长按清空」只会退化成单删一位,P2 的根因);
|
||
* app.c 在锁屏为顶层页时注册 no-op 回调后,按住 700ms(≥500 阈值)应真正产出 LONG_PRESS,
|
||
* 并按主循环顺序先交 view 分发(锁屏页消费),且其抬手不再补一个 TAP。 */
|
||
meow_view_page_push(ACT_LOCK);
|
||
int bx = 0, by = 0;
|
||
CHECK(lock_key_center(lock, LK_KBACK, &bx, &by), "backspace key has a rect");
|
||
|
||
meow_ui_set_long_press_cb(NULL);
|
||
meow_input_touch_set(1, bx, by, 700);
|
||
CHECK(meow_input_long_press_poll() == 0,
|
||
"no LONG_PRESS while callback unregistered (opt-in gate = P2 root cause)");
|
||
meow_input_touch_release();
|
||
meow_input_drain(); /* 未成长按的这次按压,抬手会补一个 TAP,清掉 */
|
||
|
||
lock_type(lock, "135");
|
||
meow_ui_set_long_press_cb(app_long_press_noop); /* 同 app.c:锁屏为顶层页时注册 */
|
||
meow_input_touch_set(1, bx, by, 700);
|
||
CHECK(meow_input_long_press_poll() == 1, "registered long-press crosses the 500ms threshold");
|
||
meow_input_ev lp = { 0, 0, 0 };
|
||
CHECK(meow_input_pop(&lp) == 1 && lp.type == MEOW_EV_LONG_PRESS &&
|
||
lp.a == bx && lp.b == by, "LONG_PRESS queued at the pressed backspace key");
|
||
CHECK(meow_view_dispatch_event(MEOW_EV_LONG_PRESS, lp.a, lp.b) == 1,
|
||
"lock view consumes LONG_PRESS before the no-op callback");
|
||
meow_input_touch_release();
|
||
CHECK(meow_input_pop(&lp) == 0, "long-press release does not emit a stray TAP");
|
||
/* 「到底清没清空」不在这里断言:此刻缓冲只有 "135"(3 位),而确认在 <4 位时本就
|
||
* disabled,任何「仍停在 ACT_LOCK」的判断都恒真、测不出清空(M30 评审 P2-1)。
|
||
* 真正的判据是下一段:若没清空,缓冲会变成 "135"+"1357" = 7 位错码 → 校验失败。 */
|
||
lock_type(lock, "1357");
|
||
lock_tap_key(lock, LK_KOK);
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY,
|
||
"unlock after long-press clear (7-digit buffer would fail if not cleared)");
|
||
|
||
/* 解锁后 app.c 关掉长按:不得把长按语义泄漏到书库/阅读器(按住稍久的点击仍算 TAP)。 */
|
||
meow_ui_set_long_press_cb(NULL);
|
||
meow_input_touch_set(1, bx, by, 700);
|
||
CHECK(meow_input_long_press_poll() == 0, "long-press off again after leaving the lock page");
|
||
meow_input_touch_release();
|
||
meow_input_drain();
|
||
|
||
/* ---- 设置页两遍输入开启 PIN(MEOW_IME_NUM) ---- */
|
||
mr_pin_disable();
|
||
meow_view_page_push(ACT_SETTINGS);
|
||
CHECK(meow_ui_activity_active() == ACT_SETTINGS, "settings active for PIN enable");
|
||
settings_tap_pin_row();
|
||
CHECK(mr_pin_enabled() == 1, "two-pass entry enabled the PIN");
|
||
CHECK(mr_pin_verify("2468") == 1, "PIN entered in settings verifies");
|
||
meow_view_page_pop();
|
||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back from settings to library");
|
||
|
||
/* 主题切换:app.c listener 整帧 invalidate;页面按 meow_theme_get() 重画不崩。 */
|
||
meow_theme_set_mode(MEOW_THEME_LIGHT);
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||
meow_theme_set_mode(MEOW_THEME_DARK);
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||
CHECK(1, "theme switch repaint does not crash");
|
||
|
||
mr_pin_disable();
|
||
meow_view_free(lock);
|
||
meow_view_free(settings);
|
||
meow_view_free(lib);
|
||
meow_ui_shutdown();
|
||
cleanup_cfg(data);
|
||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||
g_fail ? "see failures above" : "integration checks passed");
|
||
return g_fail ? 1 : 0;
|
||
}
|