用户要求:文件夹拆开可进之后仍保留进度,但入口只在**进入了该文件夹**之后才出现, 不在上层列表的文件夹行加角标/按钮。 - 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 的全部边界。
541 lines
25 KiB
C
541 lines
25 KiB
C
/* 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;
|
||
}
|