/* 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 状态变化、 * 主题模式切换后整帧重绘不崩,以及**真实 MEOW_IME_NUM 键盘**驱动的两条端到端路径: * 锁屏错 PIN(留在锁屏)→ 对 PIN(page_pop 回书库)、设置页两遍输入开启 PIN。 * * 编译:见 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 #include #include #include #include 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; } static void lock_try_pin(const char *digits) { int sb = meow_ui_statusbar_height(); int cy = sb + (meow_ui_height() - sb) / 2; pin_job job = { digits, 1 }; pthread_t th; if (pthread_create(&th, NULL, pin_injector, &job) != 0) return; meow_view_dispatch_event(MEOW_EV_TAP, 240, cy + 40); /* 点「输入 PIN」按钮 */ pthread_join(th, NULL); } 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 == 1, "lock built its button on enter (got %d)", lock->nwidgets); CHECK(meow_widget_owner(lock->widgets[0]) == ACT_LOCK, "lock button owned by ACT_LOCK"); 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"); /* ---- 锁屏真解锁路径:点按钮 → MEOW_IME_NUM 输入 → 校验 → pop(错误则留在锁屏) ---- */ lock_try_pin("1358"); /* 错 PIN:留在锁屏 */ CHECK(meow_ui_activity_active() == ACT_LOCK, "wrong PIN keeps lock (retry allowed)"); lock_try_pin("1357"); /* 正确:page_pop 回书库 */ CHECK(meow_ui_activity_active() == ACT_LIBRARY, "correct PIN pops lock to library"); /* ---- 设置页两遍输入开启 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; }