/* test_mode_switch_host.c — 阅读器模式/续读/跳页/册间衔接的宿主集成测试(可选,需 SDK 源码树)。 * * 只做纯逻辑测试覆盖不到的接缝:真实 SDK view/activity/widget + 真实图片解码 + 真实 * meow_scroll/meow_anim 节拍。用 40x60 BMP 当页图(stb 可解,无外部依赖),在临时目录 * 建两本相邻册,然后模拟用户操作: * H1 进入:默认翻页模式、页码 0、控件归属 ACT_READER(顶栏在 on_enter 建); * H2 chrome 图标一键切模式:翻页 ⇄ 条漫,切换立即写 meow_config(每册记忆); * H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳); * H4 离开/重进:恢复上次页 + 模式(续读); * H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时 * 停在原地(不退出、不崩); * H6 册间衔接根锁定:浏览根内没有相邻册时,不跨到根外的兄弟册(M18 评审观察项)。 * * 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。 */ #include "tuku_internal.h" #include "ui/reader/reader.h" #include "include/meow_ui_anim.h" #include "meow_ui_internal.h" /* MEOW_EV_TAP / MEOW_EV_SWIPE(宿主测试内部头) */ #include #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; } /* ---- 临时目录 + 最小 BMP24(stb 可解) ---- */ static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); } static void put32(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24); } static int write_bmp24(const char *path, int w, int h) { int stride = ((w * 3 + 3) / 4) * 4; int imgsz = stride * h; unsigned char fh[14] = { 0 }, ih[40] = { 0 }; fh[0] = 'B'; fh[1] = 'M'; put32(fh + 2, (unsigned)(54 + imgsz)); put32(fh + 10, 54); put32(ih + 0, 40); put32(ih + 4, (unsigned)w); put32(ih + 8, (unsigned)h); put16(ih + 12, 1); put16(ih + 14, 24); put32(ih + 20, (unsigned)imgsz); FILE *f = fopen(path, "wb"); if (!f) return -1; int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh && fwrite(ih, 1, sizeof ih, f) == sizeof ih; unsigned char *row = (unsigned char *)calloc(1, (size_t)stride); if (!row) ok = 0; for (int y = 0; ok && y < h; y++) if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0; free(row); if (fclose(f) != 0) ok = 0; return ok ? 0 : -1; } static int mkdir_p(const char *path) { char tmp[MR_PATH_MAX]; snprintf(tmp, sizeof tmp, "%s", path); for (size_t i = 1; i <= strlen(tmp); i++) { if (tmp[i] == '/' || tmp[i] == '\0') { char ch = tmp[i]; tmp[i] = '\0'; if (mkdir(tmp, 0755) != 0) { struct stat st; if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1; } tmp[i] = ch; } } return 0; } static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) return -1; if (!S_ISDIR(st.st_mode)) return unlink(path); DIR *d = opendir(path); if (!d) return -1; struct dirent *de; while ((de = readdir(d)) != NULL) { if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; char full[MR_PATH_MAX]; snprintf(full, sizeof full, "%s/%s", path, de->d_name); rm_rf(full); } closedir(d); return rmdir(path); } /* 驱动 SDK 动画节拍(宿主没有 meow_ui_run 主循环)。 */ static void pump_ms(int ms) { for (int i = 0; i < ms / 10; i++) { usleep(10 * 1000); meow_anim_tick(); } } /* 模拟 app.c 的点击路由:on_touch_down → on_tap(阅读页 tap 不在 view on_event 里)。 */ static int tap(int x, int y) { reader_on_touch_down(x, y); reader_on_tap(x, y); return 1; } static int swipe(int dir, int dist) { return meow_view_dispatch_event(MEOW_EV_SWIPE, dir, dist); } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "."; char tmpl[512]; snprintf(tmpl, sizeof tmpl, "%s/mr_mode_host_XXXXXX", base); char *data = mkdtemp(tmpl); if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; } setenv("MEOW_DATA_DIR", data, 1); char root[MR_PATH_MAX], book1[MR_PATH_MAX], book2[MR_PATH_MAX]; snprintf(root, sizeof root, "./mr_mode_books_%ld", (long)getpid()); snprintf(book1, sizeof book1, "%s/vol1", root); snprintf(book2, sizeof book2, "%s/vol2", root); mkdir_p(book1); mkdir_p(book2); for (int b = 0; b < 2; b++) { const char *dir = b ? book2 : book1; for (int i = 0; i < 3; i++) { char p[MR_PATH_MAX]; snprintf(p, sizeof p, "%s/p%d.bmp", dir, i + 1); if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: bmp write failed\n"); return 0; } } } 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 *reader = reader_view_create(); meow_view_page_push(ACT_LIBRARY); /* 阅读器册间衔接的浏览根:测试夹具根(生产默认 MR_ROOT_DIR)。 */ mr_reader_test_set_root(root); CHECK(strcmp(mr_reader_test_root(), root) == 0, "reader browse root installed"); /* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */ int w = meow_ui_width(); int mode_x = w - 10 - 48 * 3 + 24; int jump_x = w - 10 - 48 * 2 + 24; int bar_y = 28; int slider_y = 56 + 4 + 20; int sx = 24, sw = w - 48; /* ---- H1 进入:翻页模式、第 1 页 ---- */ mr_reader_open_dir(book1); CHECK(meow_ui_activity_active() == ACT_READER, "reader active after open"); CHECK(mr_reader_ui_count() == 3, "book pages=%d", mr_reader_ui_count()); CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "default mode is paged"); CHECK(mr_reader_ui_page() == 0, "start at page 0"); pump_ms(300); /* ---- H2 模式切换:chrome 图标一键切,立即持久化 ---- */ CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon"); CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON, "webtoon mode persisted per book"); CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (back)"); CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "switched back to paged"); CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_PAGED, "paged mode persisted"); CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (again)"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon again for continuity"); /* ---- H3 跳页:滑块展开 → 拖到末页 → 预解码完成后落位 ---- */ CHECK(tap(jump_x, bar_y) == 1, "jump icon tap consumed"); CHECK(mr_reader_ui_jump_open() == 1, "slider open"); reader_on_touch_down(sx + sw, slider_y); reader_on_tap(sx + sw, slider_y); CHECK(mr_reader_ui_jump_open() == 0, "slider closed after pick"); CHECK(mr_reader_ui_jump_pending() == 1 || mr_reader_ui_page() == 2, "jump pending (waiting for target decode)"); pump_ms(600); CHECK(mr_reader_ui_jump_pending() == 0, "jump landed"); CHECK(mr_reader_ui_page() == 2, "jumped to last page (got %d)", mr_reader_ui_page()); /* ---- H4 续读:离开保存 page+mode,重进恢复 ---- */ CHECK(swipe(MEOW_SWIPE_LEFT, 200) == 0, "reader leaves back-swipe to app.c"); meow_view_page_pop(); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library"); CHECK(mr_cont_get_page(book1, -1) == 2, "last page saved (got %d)", mr_cont_get_page(book1, -1)); CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON, "mode saved"); CHECK(g_lib_enters >= 2, "library re-entered"); mr_reader_open_dir(book1); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode restored on reopen"); CHECK(mr_reader_ui_page() == 2, "page restored on reopen (got %d)", mr_reader_ui_page()); pump_ms(300); /* ---- H5 册间衔接:条漫末页上拉 → 下一册首页 ---- */ reader_on_touch_down(240, 600); for (int i = 1; i <= 6; i++) { reader_on_touch_move(240, 600 - i * 40); usleep(5 * 1000); } CHECK(swipe(MEOW_SWIPE_UP, 240) == 1, "webtoon up-swipe consumed"); CHECK(strcmp(mr_reader_dir(), book2) == 0, "advanced to next book (got %s)", mr_reader_dir()); CHECK(mr_reader_ui_page() == 0, "next book starts at page 0"); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode carried into fresh book"); pump_ms(300); /* 首页下拉 → 上一册末页 */ reader_on_touch_down(240, 300); for (int i = 1; i <= 6; i++) { reader_on_touch_move(240, 300 + i * 40); usleep(5 * 1000); } CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "webtoon down-swipe consumed"); CHECK(strcmp(mr_reader_dir(), book1) == 0, "back to previous book (got %s)", mr_reader_dir()); CHECK(mr_reader_ui_page() == 2, "previous book lands at last page (got %d)", mr_reader_ui_page()); pump_ms(300); /* 跳到首页后,再往上一册:没有相邻册,停在原地并提示(不退出/不崩)。 */ CHECK(tap(jump_x, bar_y) == 1, "jump icon tap consumed (to first page)"); CHECK(mr_reader_ui_jump_open() == 1, "slider open again"); reader_on_touch_down(sx + 2, slider_y); reader_on_tap(sx + 2, slider_y); pump_ms(500); CHECK(mr_reader_ui_page() == 0, "jumped to first page (got %d)", mr_reader_ui_page()); reader_on_touch_down(240, 300); for (int i = 1; i <= 6; i++) { reader_on_touch_move(240, 300 + i * 40); usleep(5 * 1000); } CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "edge down-swipe consumed"); CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put"); CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)"); /* ---- H6 册间衔接根锁定:浏览根=book2 时,兄弟册 book1 在根外,不得翻过去 ---- */ meow_view_page_pop(); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library for root-lock case"); mr_reader_test_set_root(book2); CHECK(strcmp(mr_reader_test_root(), book2) == 0, "reader root narrowed to book2"); mr_reader_open_dir_at(book2, 0); /* 显式首页,避开续读页差异 */ CHECK(meow_ui_activity_active() == ACT_READER, "reopened book2"); CHECK(strcmp(mr_reader_dir(), book2) == 0, "at book2"); if (mr_reader_ui_mode() != MR_MODE_WEBTOON) mr_reader_test_toggle_mode(); CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon for root-lock case"); CHECK(mr_reader_ui_page() == 0, "root-lock case at first page (got %d)", mr_reader_ui_page()); /* 首页下拉 → book1 是 book2 的上一册,但在浏览根外:必须原地不动。 */ reader_on_touch_down(240, 300); for (int i = 1; i <= 6; i++) { reader_on_touch_move(240, 300 + i * 40); usleep(5 * 1000); } CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "root-lock down-swipe consumed"); CHECK(strcmp(mr_reader_dir(), book2) == 0, "adjacent book outside browse root is refused (dir=%s)", mr_reader_dir()); CHECK(meow_ui_activity_active() == ACT_READER, "root-lock: still in reader"); mr_reader_test_set_root(root); /* 复原夹具根 */ /* ---- 收尾 ---- */ meow_view_page_pop(); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library"); meow_view_free(reader); meow_view_free(lib); meow_ui_shutdown(); rm_rf(root); rm_rf(data); printf("%s: %s\n", g_fail ? "FAIL" : "PASS", g_fail ? "see failures above" : "reader mode/continuity integration checks passed"); return g_fail ? 1 : 0; }