- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页); 排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测) - 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化 (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码 - 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」 - 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出 - 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示) - 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判, 超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险) - 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的 阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
274 lines
11 KiB
C
274 lines
11 KiB
C
/* 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 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时
|
||
* 停在原地(不退出、不崩)。
|
||
*
|
||
* 编译: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 <dirent.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
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);
|
||
|
||
/* 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)");
|
||
|
||
/* ---- 收尾 ---- */
|
||
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;
|
||
}
|