/* test_browse_ui_host.c — 书库浏览页宿主集成测试(可选,需 SDK 源码树)。 * * 目的(M18 文件管理器模型): * U1 行模型:目录 + 图片 + 档案册混排(目录优先+自然序),行副标题是扫描期生成的 * 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分; * U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问); * U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读 * (mr_reader_open_dir_at_name(当前目录, 文件名);阅读器自己定位下标); * 点档案册=打开档案册; * U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出); * 子目录返回=回根;不存在越过浏览根的导航; * U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」, * 不弹错误横幅;顶栏刷新/重扫可重试成功; * U6 子目录不可读:停留原目录 + 顶部提示横幅; * U7 书库不再碰缓存目录:CBZ 解压页缓存 mrpage_* 原样保留,且不写任何 bmp。 * U8 目录内续读入口(M33):顶栏「续读」只在当前所在目录有进度时出现(上层列表行 * 不加角标),动作数 3/2 随导航切换;点按从续读页打开当前目录;保存页超页数 * 钳到末页;空目录/无进度不显示;从阅读器返回后重新判定。 * * 编译:见 test/run_tests.sh(链 SDK 源码树 + 打桩;只发布 meow_ui.a 的环境自动 SKIP)。 * ui/library/library.c 以 -DMR_LIBRARY_UI_TEST_HOOKS 编译出测试接缝;logic/library.c * 以 -DMR_LIBRARY_TEST_HOOKS 编译出 IO 计数。 */ #include "tuku_internal.h" #include "logic/library.h" #include "logic/reader/page_cache.h" #include #include #include #include #include #include #include /* ui/library/library.c 的测试接缝(仅 -DMR_LIBRARY_UI_TEST_HOOKS 时定义) */ int library_test_load(const char *dir); void library_test_set_root(const char *root); const char *library_test_root(void); const char *library_test_dir(void); void library_test_set_dir(const char *dir); int library_test_rows(void); int library_test_src(int idx, meow_list_item *item); int library_test_hint_active(void); const char *library_test_hint(void); int library_test_topbar_back(void); const char *library_test_empty_text(void); int library_test_sd_missing(void); int library_test_retry(void); int library_test_go_up(void); int library_test_drag_active(void); int library_test_has_list(void); int library_test_topbar_actions(void); int library_test_resume_page(void); int library_test_topbar_hit(int x, int y); /* 顶栏命中枚举(与 ui/library/library.c 的 LIB_TB_* 同值)。 */ #define LIB_TB_NONE 0 #define LIB_TB_BACK 1 #define LIB_TB_RESUME 2 #define LIB_TB_REFRESH 3 #define LIB_TB_SETTINGS 4 /* 阅读器入口打桩:记录被打开的册路径 / 起始页 / 按名定位的文件名(本测试不建 reader 页)。 */ #define MR_READER_START_RESUME (-1) void mr_reader_open_dir_at(const char *dir, int start_index); void mr_reader_open_dir_at_name(const char *dir, const char *name); static char g_opened[MR_PATH_MAX]; static char g_opened_name[MR_NAME_MAX]; static int g_opened_start; static int g_opened_n; void mr_reader_open_dir_at(const char *dir, int start_index) { snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : ""); g_opened_name[0] = '\0'; g_opened_start = start_index; g_opened_n++; } void mr_reader_open_dir_at_name(const char *dir, const char *name) { snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : ""); snprintf(g_opened_name, sizeof g_opened_name, "%s", name ? name : ""); g_opened_start = MR_READER_START_RESUME; g_opened_n++; } 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 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 touch_file(const char *path) { FILE *f = fopen(path, "wb"); if (!f) return -1; fputs("x", f); return fclose(f); } static int rm_rf(const char *path) { struct stat st; if (lstat(path, &st) != 0) return -1; if (!S_ISDIR(st.st_mode)) { chmod(path, 0700); return unlink(path); } chmod(path, 0700); 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); } static void joinp(char *out, int outsz, const char *a, const char *b) { snprintf(out, (size_t)outsz, "%s/%s", a, b); } static int row_index_for(const char *name) { int rows = library_test_rows(); for (int i = 0; i < rows; i++) { meow_list_item it; if (!library_test_src(i, &it)) continue; if (it.title && strcmp(it.title, name) == 0) return i; } return -1; } static void tap_row(const char *name) { int idx = row_index_for(name); if (idx < 0) { CHECK(0, "row %s not found", name); return; } int list_top = meow_ui_statusbar_height() + 56; library_on_tap(120, list_top + idx * 56 + 28); } /* 统计缓存目录里的 bmp(缩略图/解压页)。 */ static int count_bmp(const char *dir, int *pages) { DIR *d = opendir(dir); if (!d) return -1; int thumbs = 0, pg = 0; struct dirent *de; while ((de = readdir(d)) != NULL) { size_t len = strlen(de->d_name); if (len <= 4 || strcmp(de->d_name + len - 4, ".bmp") != 0) continue; if (mr_cache_is_page_name(de->d_name)) pg++; else thumbs++; } closedir(d); if (pages) *pages = pg; return thumbs; } int main(void) { const char *base = getenv("TMPDIR"); if (!base || !base[0]) base = "."; char tmpl[512]; snprintf(tmpl, sizeof tmpl, "%s/mr_browse_ui_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], p[MR_PATH_MAX]; snprintf(root, sizeof root, "%s/library", data); const char *dirs[] = { "album", "empty", "noimg", "deep/sub", "Pictures", "Pictures/sub" }; for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) { joinp(p, sizeof p, root, dirs[i]); if (mkdir_p(p) != 0) { printf("SKIP: mkdir %s failed\n", p); return 0; } } joinp(p, sizeof p, root, "album/001.png"); touch_file(p); joinp(p, sizeof p, root, "album/002.png"); touch_file(p); joinp(p, sizeof p, root, "noimg/readme.txt"); touch_file(p); joinp(p, sizeof p, root, "deep/sub/001.png"); touch_file(p); joinp(p, sizeof p, root, "root.png"); touch_file(p); joinp(p, sizeof p, root, "book.cbz"); touch_file(p); /* Pictures:大小写混排 + 数字序号 + 大写扩展名 + 档案册(图片排序契约夹具)。 */ const char *pics[] = { "A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png", "page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt" }; for (unsigned i = 0; i < sizeof pics / sizeof pics[0]; i++) { joinp(p, sizeof p, root, "Pictures"); size_t dl = strlen(p); snprintf(p + dl, sizeof p - dl, "/%s", pics[i]); touch_file(p); } meow_ui_opts o = { 480, 720 }; if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; } /* ---- U5:浏览根不可访问 —— 先指向不存在的根,再 create ---- */ const char *bad_root = "/no/such/tuku/m18_root"; library_test_set_root(bad_root); CHECK(strcmp(library_test_root(), bad_root) == 0, "bad root installed"); meow_view *lib = library_view_create(); CHECK(library_test_sd_missing() == 1, "unreadable root -> SD missing state"); CHECK(library_test_rows() == 0, "empty list rows=%d", library_test_rows()); CHECK(library_test_has_list(), "startup still builds a listview"); CHECK(strcmp(library_test_dir(), bad_root) == 0, "startup dir=%s", library_test_dir()); CHECK(strstr(library_test_empty_text(), "SD") != NULL, "friendly empty state: %s", library_test_empty_text()); CHECK(library_test_hint_active() == 0, "no error banner for missing SD"); CHECK(library_test_topbar_back() == 0, "no back arrow at storage root"); CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1, "retry while still missing stays in empty state (no crash)"); /* 根路径不是目录(普通文件)同样走 SD 空态。 */ char notadir[MR_PATH_MAX]; joinp(notadir, sizeof notadir, data, "notadir"); touch_file(notadir); library_test_set_root(notadir); library_test_set_dir(notadir); CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1, "root pointing at a file -> SD missing state"); /* 插卡/挂载恢复:同一路径变成可读目录后,刷新即恢复列表。 */ library_test_set_root(root); library_test_set_dir(root); CHECK(library_test_retry() > 0, "retry after storage available loads rows"); CHECK(library_test_sd_missing() == 0, "SD missing state cleared"); CHECK(strcmp(library_test_dir(), root) == 0, "now browsing root"); CHECK(library_test_topbar_back() == 0, "still no back arrow at root"); meow_view_page_push(ACT_LIBRARY); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "library active"); /* ---- U1:行模型(目录优先 + 自然序 + 类型图标/箭头) ---- */ CHECK(library_test_load(root) == 1, "load fixture root"); CHECK(library_test_rows() == 7, "7 rows (got %d)", library_test_rows()); static const char *want[7] = { "album", "deep", "empty", "noimg", "Pictures", "book.cbz", "root.png" }; for (int i = 0; i < 7; i++) { meow_list_item it; CHECK(library_test_src(i, &it) == 1, "src row %d", i); CHECK(it.title && strcmp(it.title, want[i]) == 0, "row[%d]=%s (want %s)", i, it.title ? it.title : "(null)", want[i]); CHECK(it.subtitle && it.subtitle[0] && strcmp(it.subtitle, "…") != 0, "row[%d] subtitle pre-generated (%s)", i, it.subtitle ? it.subtitle : ""); if (i < 5) { CHECK(it.icon_kind == MEOW_LIST_ICON_FOLDER && it.right == MEOW_LIST_RIGHT_ARROW, "row[%d] is a folder row with enter arrow", i); } else { CHECK(it.icon_kind == MEOW_LIST_ICON_FILE && it.right == MEOW_LIST_RIGHT_NONE, "row[%d] is a file row without child arrow", i); } } int ic = row_index_for("book.cbz"); meow_list_item it; CHECK(ic >= 0 && library_test_src(ic, &it) && strncmp(it.subtitle, "档案册 · ", strlen("档案册 · ")) == 0, "archive subtitle=%s", it.subtitle ? it.subtitle : ""); int ir = row_index_for("root.png"); CHECK(ir >= 0 && library_test_src(ir, &it) && strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0, "image subtitle=%s", it.subtitle ? it.subtitle : ""); /* ---- U2:行合成零 IO(渲染/滚动路径不访问文件系统) ---- */ mr_library_test_io_reset(); for (int pass = 0; pass < 500; pass++) for (int i = 0; i < library_test_rows(); i++) { meow_list_item tmp; library_test_src(i, &tmp); } mr_library_io_stats io; mr_library_test_io_get(&io); CHECK(io.opendirs == 0 && io.readdirs == 0 && io.lstats == 0, "row source does no IO (open=%ld rd=%ld stat=%ld)", io.opendirs, io.readdirs, io.lstats); /* ---- U3:点击语义 ---- */ char pics_dir[MR_PATH_MAX]; joinp(pics_dir, sizeof pics_dir, root, "Pictures"); CHECK(library_test_load(pics_dir) == 1, "entered Pictures"); CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir()); /* 点图片:把**文件名**交给阅读器(阅读器用自己的全量目录册定位起始页; * 书库列表可能被 MR_LIB_SCAN_MAX 截断,所以不再传下标)。 */ const char *target = "img10.png"; int ti = row_index_for(target); CHECK(ti >= 0, "target image row present"); g_opened_n = 0; tap_row(target); CHECK(g_opened_n == 1 && strcmp(g_opened, pics_dir) == 0, "tap image opens reader for current dir (%s)", g_opened); CHECK(strcmp(g_opened_name, target) == 0, "reader gets file name (%s)", g_opened_name); CHECK(g_opened_start == MR_READER_START_RESUME, "name-based open does not pass an index"); CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image does not navigate"); g_opened_n = 0; tap_row("cover.cbz"); char archive[MR_PATH_MAX]; joinp(archive, sizeof archive, pics_dir, "cover.cbz"); CHECK(g_opened_n == 1 && strcmp(g_opened, archive) == 0, "tap archive opens archive book (%s)", g_opened); CHECK(g_opened_start == MR_READER_START_RESUME, "archive uses resume start (%d)", g_opened_start); /* 点目录 = 进入;空目录也进(不打开阅读器)。 */ char sub_dir[MR_PATH_MAX]; joinp(sub_dir, sizeof sub_dir, pics_dir, "sub"); g_opened_n = 0; tap_row("sub"); CHECK(g_opened_n == 0, "tap dir does not open reader"); CHECK(strcmp(library_test_dir(), sub_dir) == 0, "entered empty sub dir (%s)", library_test_dir()); CHECK(library_test_rows() == 0, "empty dir shows empty list"); CHECK(strcmp(library_test_empty_text(), "空文件夹") == 0, "empty dir state: %s", library_test_empty_text()); /* ---- U4:根目录锁定 ---- */ char album_dir[MR_PATH_MAX]; joinp(album_dir, sizeof album_dir, root, "album"); CHECK(library_test_load(album_dir) == 1, "load album"); CHECK(library_test_topbar_back() == 1, "back arrow shown in subdir"); CHECK(library_test_go_up() == 1, "go_up from subdir consumed"); CHECK(strcmp(library_test_dir(), root) == 0, "back to root"); /* 根:系统返回 = 退出(返回 0),并保持列表不变。 */ CHECK(library_test_go_up() == 0, "go_up at root returns 0 (app exits)"); CHECK(strcmp(library_test_dir(), root) == 0, "still at root after exit gesture"); CHECK(library_test_topbar_back() == 0, "no back arrow at root"); /* 顶栏返回:子目录点击返回箭头 = 回根;根上该区域不响应。 */ CHECK(library_test_load(album_dir) == 1, "load album again"); library_on_tap(20, meow_ui_statusbar_height() + 28); CHECK(strcmp(library_test_dir(), root) == 0, "topbar back returns to root"); int root_rows_before = library_test_rows(); library_on_tap(20, meow_ui_statusbar_height() + 28); CHECK(strcmp(library_test_dir(), root) == 0 && library_test_rows() == root_rows_before, "topbar back is inert at root"); /* 嵌套:deep/sub 两级返回。 */ tap_row("deep"); tap_row("sub"); char deep_sub[MR_PATH_MAX], deep_dir[MR_PATH_MAX]; joinp(deep_dir, sizeof deep_dir, root, "deep"); joinp(deep_sub, sizeof deep_sub, root, "deep/sub"); CHECK(strcmp(library_test_dir(), deep_sub) == 0, "entered deep/sub (%s)", library_test_dir()); CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), deep_dir) == 0, "up to deep"); CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root"); /* ---- U3c:左缘 24px / 屏底热区不启动列表拖拽 ---- */ int list_top = meow_ui_statusbar_height() + 56; int row_y = list_top + 28; library_on_touch_down(120, row_y); CHECK(library_test_drag_active() == 1, "touch inside list starts drag"); library_on_touch_down(5, row_y); CHECK(library_test_drag_active() == 0, "left-edge touch does not start drag"); library_on_touch_down(23, row_y); CHECK(library_test_drag_active() == 0, "left-edge (23px) does not start drag"); library_on_touch_down(24, row_y); CHECK(library_test_drag_active() == 1, "just outside left band starts drag"); library_on_touch_down(120, meow_ui_height() - 4); CHECK(library_test_drag_active() == 0, "bottom hotzone does not start drag"); /* ---- U6:不可读子目录 → 停留 + 提示 ---- */ if (geteuid() != 0) { char noread[MR_PATH_MAX]; joinp(noread, sizeof noread, root, "noread"); CHECK(mkdir(noread, 0755) == 0, "mkdir noread"); chmod(noread, 0000); int ok = library_test_load(noread); CHECK(ok == 0, "unreadable dir load fails"); CHECK(strcmp(library_test_dir(), root) == 0, "stays in previous dir (%s)", library_test_dir()); CHECK(library_test_hint_active(), "hint banner shown: %s", library_test_hint()); CHECK(strstr(library_test_hint(), "无法读取") != NULL, "hint text mentions unreadable: %s", library_test_hint()); chmod(noread, 0700); } else { printf(" SKIP unreadable-dir case (running as root)\n"); } /* ---- U7:书库不碰缓存目录(CBZ 解压页缓存原样保留,不写 bmp) ---- */ CHECK(mr_cache_init(data) == 0, "page cache init"); const char *cdir = mr_cache_dir(); CHECK(cdir && cdir[0], "page cache dir: %s", cdir ? cdir : ""); if (cdir && cdir[0]) { char page[MR_PATH_MAX + 96]; snprintf(page, sizeof page, "%s/mrpage_0123456789abcdef.bmp", cdir); CHECK(touch_file(page) == 0, "create page cache file"); int pages_before = 0; count_bmp(cdir, &pages_before); CHECK(pages_before == 1, "precondition 1 page (got %d)", pages_before); CHECK(library_test_load(root) == 1, "reload root"); for (int i = 0; i < library_test_rows(); i++) { meow_list_item tmp; library_test_src(i, &tmp); } tap_row("album"); tap_row("001.png"); /* 打开阅读器入口(打桩) */ library_test_go_up(); int pages_after = 0; int thumbs_after = count_bmp(cdir, &pages_after); CHECK(access(page, R_OK) == 0, "mrpage_* survives book browsing"); CHECK(pages_after == 1 && thumbs_after == 0, "library writes no bmp to cache (pages=%d thumbs=%d)", pages_after, thumbs_after); } /* ---- U8:目录内续读入口(M33) ---- * 顶栏「续读」只在**已进入的目录本身**有阅读进度时出现(不在上层列表的文件夹行 * 加角标/按钮——用户明确要求「进入了该文件夹才有显示」)。点按 = 从续读页打开 * 当前目录;保存页超出当前图片数(文件被删)→ 钳到末页;空目录/无进度不显示。 */ const int tby = meow_ui_statusbar_height() + 28; char empty_dir[MR_PATH_MAX]; joinp(empty_dir, sizeof empty_dir, root, "empty"); /* 顶栏宽 480、动作从右往左各 48px:设置最右 [422,470)、刷新 [374,422)、 * 续读(有进度时)[326,374)。 */ const int x_resume = 350, x_refresh = 395, x_settings = 440; /* 根没有进度:只有刷新+设置两个动作,续读格不存在(不是置灰占位)。 */ CHECK(library_test_load(root) == 1, "U8 load root"); CHECK(library_test_topbar_actions() == 2, "no-progress dir has 2 actions (got %d)", library_test_topbar_actions()); CHECK(library_test_resume_page() == -1, "no resume page without progress"); CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_NONE, "no resume hit cell when hidden"); CHECK(library_test_topbar_hit(x_refresh, tby) == LIB_TB_REFRESH, "refresh cell intact without resume"); CHECK(library_test_topbar_hit(x_settings, tby) == LIB_TB_SETTINGS, "settings cell intact without resume"); /* album 有 2 张图;写入进度 P2(page=1)后进入 → 3 个动作。 */ mr_cont_set_page(album_dir, 1); CHECK(library_test_load(album_dir) == 1, "U8 load album"); CHECK(library_test_topbar_actions() == 3, "dir with progress has 3 actions (got %d)", library_test_topbar_actions()); CHECK(library_test_resume_page() == 1, "resume page=%d (want 1)", library_test_resume_page()); CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_RESUME, "resume cell hits"); CHECK(library_test_topbar_hit(x_refresh, tby) == LIB_TB_REFRESH, "refresh still hits with 3 actions"); CHECK(library_test_topbar_hit(x_settings, tby) == LIB_TB_SETTINGS, "settings still hits with 3 actions"); CHECK(library_test_topbar_hit(20, tby) == LIB_TB_BACK, "back cell still hits"); /* 点续读 = 从当前目录的续读页打开(阅读器按自己的全量目录册再夹持一次)。 */ g_opened_n = 0; g_opened[0] = '\0'; g_opened_name[0] = '\0'; g_opened_start = 12345; library_on_tap(x_resume, tby); CHECK(g_opened_n == 1 && strcmp(g_opened, album_dir) == 0, "tap resume opens current dir (%s)", g_opened); CHECK(g_opened_start == MR_READER_START_RESUME, "resume tap uses resume start (%d)", g_opened_start); CHECK(strcmp(library_test_dir(), album_dir) == 0, "resume tap does not navigate"); /* 顶栏刷新仍重扫当前目录(下拉/刷新后可见性保持)。 */ char extra[MR_PATH_MAX]; joinp(extra, sizeof extra, album_dir, "003.png"); touch_file(extra); library_on_tap(x_refresh, tby); CHECK(library_test_rows() == 3, "refresh rescans dir after resume (rows=%d)", library_test_rows()); CHECK(library_test_topbar_actions() == 3, "resume survives refresh"); unlink(extra); /* 返回上层:上层没有这层的进度 → 续读隐藏;再进入 → 重新出现。 */ CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root"); CHECK(library_test_topbar_actions() == 2, "resume hidden in parent without progress"); CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_NONE, "resume cell gone in parent"); CHECK(library_test_load(album_dir) == 1, "re-enter album"); CHECK(library_test_topbar_actions() == 3, "resume back after re-enter"); /* 根也是目录:根有进度时同样显示(root.png 一页,page 0 是有效记录)。 */ mr_cont_set_page(root, 0); CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root again"); CHECK(library_test_topbar_actions() == 3, "root shows resume when it has progress"); CHECK(library_test_resume_page() == 0, "root resume page=%d (want 0)", library_test_resume_page()); /* 边界:保存页超出当前图片数(文件被删)→ 钳到末页(2 页 → 下标 1),仍显示。 */ mr_cont_set_page(album_dir, 99); CHECK(library_test_load(album_dir) == 1, "load album with stale page"); CHECK(library_test_resume_page() == 1, "stale page clamps to last (got %d)", library_test_resume_page()); CHECK(library_test_topbar_actions() == 3, "clamped resume still visible"); /* 空目录:即使有残留记录也不显示(没有可续读的页)。 */ mr_cont_set_page(empty_dir, 2); CHECK(library_test_load(empty_dir) == 1, "load empty dir"); CHECK(library_test_resume_page() == -1, "empty dir has no resume entry"); CHECK(library_test_topbar_actions() == 2, "empty dir hides resume (got %d)", library_test_topbar_actions()); /* 从阅读器返回后重判:Pictures 本来没有进度 → 2;阅读器离开时写入进度后, * 走真实 view 生命周期(push reader → pop)→ library on_enter 重判为 3。 */ CHECK(library_test_load(pics_dir) == 1, "load Pictures without progress"); CHECK(library_test_topbar_actions() == 2, "Pictures initially 2 actions"); mr_cont_set_page(pics_dir, 2); /* 模拟阅读器离开时刚写盘 */ meow_view *faux_reader = meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, NULL, NULL); meow_view_page_push(ACT_READER); meow_view_page_pop(); CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back on library after reader"); CHECK(library_test_topbar_actions() == 3, "resume appears after reader return (got %d)", library_test_topbar_actions()); CHECK(library_test_resume_page() == 2, "resume page after return=%d", library_test_resume_page()); meow_view_free(faux_reader); meow_view_free(lib); meow_ui_shutdown(); rm_rf(data); printf("%s: %s\n", g_fail ? "FAIL" : "PASS", g_fail ? "see failures above" : "browse UI checks passed"); return g_fail ? 1 : 0; }