Files
MeowOS-SDK/tuku/test/test_browse_ui_host.c
T
AskaEth c5985b2c67 图库: 进入文件夹后顶栏显示「续读」,一键回到上次那页(M33)
用户要求:文件夹拆开可进之后仍保留进度,但入口只在**进入了该文件夹**之后才出现,
不在上层列表的文件夹行加角标/按钮。

- ui/library/library.c:顶栏按 mr_cont_resume_page(g_cur_dir, 图片数) 判定,有进度才加
  MEOW_ICON_PLAY_ARROW「续读」动作(label 保留为语义名);点按 = mr_reader_open_dir_at
  (g_cur_dir, MR_READER_START_RESUME)。SDK 的 TopAppBar 动作创建期固定、无隐藏 API,
  因此可见性变化时整条顶栏重建:无进度是真的 2 个动作(没有空占位/死点击区),
  续读插最左,刷新/设置的下标与命中区不变。
- 可见性实时重判:load_dir(进入/返回/刷新)与 library_page_enter(从阅读器返回,
  进度可能刚写盘)都调 resume_refresh;根目录有进度时同样显示。
- logic/reader.c/.h:新增 mr_cont_resume_page(dir, pages)——无记录/空目录 → -1(不显示);
  保存页超页数(文件被删)→ 钳到末页并 INFO 日志;页数未知(列表被 2000 截断)不夹持,
  上界交给阅读器 open_book 自己的 mr_nav_clamp_page。
- 不改列表行语义(不重新引入 M18 删掉的「目录自动当书」)。
- 宿主回归(test_browse_ui_host U8):有进度目录 3 个动作/无进度 2 个、三个动作的点击区
  互不遮挡、点续读从当前目录续读页打开、超页数钳到末页、空目录不显示、上层/进入/返回
  与阅读器返回后的可见性切换;test_library T5 覆盖 mr_cont_resume_page 的全部边界。
2026-11-06 19:35:43 +08:00

541 lines
25 KiB
C
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.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
/* 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;
}