Files
MeowOS-SDK/tuku/test/test_mode_switch_host.c
T
AskaEth ff97f3cee7 feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接
- 条漫: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 一键运行
2026-11-04 21:49:33 +08:00

274 lines
11 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_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;
}