Flutter 客户端(rakullapp_core)的测试分为两层 —— 表单测试功能测试 —— 以「按平台、按功能」拆分的用例清单形式编目,并用一个自带的 HTML 看板来跟踪。本页与偏后端的测试页互补。

两层测试

表单测试 (Form)

只测单个 widget 或函数本身,不启动任何服务器。喂给它假数据(用 httpMockClient),看用户最终看到什么:布局;加载 / 空 / 出错三种状态;输入校验;页面跳转;以及 JSON 解析。跑得快,用普通的 flutter test 就行。目前所有自动化客户端测试都是这一类。

功能测试 (Func)

对着一台真实在跑的服务器测 —— 只有真服务器才能验证的东西:完整登录、数据是否真的存下来了、「不能动别人的数据」这类权限规则、实时进度推送(SSE),以及前后端对请求/返回格式是否一致。
后端自己也是这样拆的 —— unit_test/(相当于我们的表单测试)和 functional_test/SSE(Server-Sent Events)是一条服务器到客户端的单向流,在长任务运行时实时推送进度(比如「处理中 40%」)。

功能测试能自动化吗?

可以 —— 有三种方式,由低成本到高成本:
  1. 接口级(最省) —— 用真实的 http.Client() 构造 StudyApi / UserApi / CollectionApi,打到本地服务栈(scripts/run_all_servers.sh)。无需模拟器;与 rakull_server 自己的 functional_test/ 思路一致。用 @Tags(['functional']) 做开关。
  2. 完整 E2E —— 引入 integration_test 包,在 CI 里无头运行(flutter test integration_test),通过 --dart-define 把各服务地址指向本地栈。
  3. 种子数据 —— 用 scripts/init_all_dbs.sh + 一个 root/邀请码种子准备确定性数据;各服务共享同一个 JWT_SECRET
仍需手动 / 只能部分自动化: Google 与 Supabase 的 OAuth 授权(交互式)、音频播放(audioplayers)与文件选择(file_picker)插件,以及 web 上的 SSE(只有最终事件)。各平台清单里都已显式标注。

用例清单目录

用例位于 rakullapp_core/test/test_cases/,先按平台、再按功能组织:
test/test_cases/
  README.md                <- 两层说明、自动化、图例、DI 阻塞项
  build_tracker.py         <- 由清单生成 tracker.html
  tracker.html             <- 网页看板(生成产物)
  web/ | mobile/ | desktop/
    auth/checklist.md
    study-login/checklist.md
    library/checklist.md
    reader/checklist.md
    upload/checklist.md
    collections/checklist.md
    admin/checklist.md
平台关注点
webBrowserClient 的 SSE(仅最终事件)、Supabase OAuth web 回退、打字机标题
mobileAndroid + iOS · 原生 Google 登录、设备文件选择、音频播放
desktopmacOS + Windows + Linux · 增量 SSE、桌面 audioplayers、鼠标选词
功能涉及的页面 / 组件
authbegining、login、connect-apps、RegisterServiceSupabaseBootstrapUserDbSyncService
study-loginstudy_login_screen、UserApi 鉴权
librarydashboard、article_list、体裁筛选
readerreader_screen、讲解/diff/编辑面板、逐句音频、收藏选择器
uploadarticle_upload、video_upload、task_progress_dialog(SSE)
collectionscollections_screen、collection_picker、CollectionApi
adminadmin_screen(用户 / 邀请码 / 申请 / 文章)

怎么读一条清单

每行末尾都有一个方括号标签,例如 — [Form · Widget+Mock · (DI: SseClient)],从左往右读:
  • 第一个词 —— FormFunc —— 属于哪一层(见上)。它也决定这一行算不算一条真正的测试;没有这个标签的行只是一条灰色备注。
  • 中间 —— 这条测试实际怎么写:
标签含义
Pure纯 Dart 单元测试,不涉及 UI
Widget渲染这个 widget,检查显示是否正确
Widget+Mock渲染 widget,背后接一个假的 API(MockClient
Widget+prefs在 widget 测试里顺带设置 SharedPreferences
MockClient用假的 HTTP 客户端测试某个 API 类
Live API打到真实在跑的服务器
integration_test完整的端到端运行
Manual / Manual/device由人在真实浏览器 / 设备上手动检查
  • (DI: …) —— 现在还测不了。 这个 widget 在内部自己 new 了依赖,测试没法换成假的;得先改成「可注入」(见可测性阻塞项)。所谓可注入,就是让 widget 通过构造函数接收它的 API / 服务,而不是自己在内部创建 —— 这样测试就能传一个假的进去。

可视化跟踪器

我们没有引入托管的测试管理工具,而是直接由清单生成一个自带的单文件 HTML 看板 —— 无需账号、无需服务器、无需联网。用浏览器打开它逐项打勾即可,进度会保存在浏览器的 localStorage 里。
cd rakullapp_core
python3 test/test_cases/build_tracker.py   # 解析清单 -> tracker.html
open test/test_cases/tracker.html          # macOS;或直接双击该文件
它会解析约 200 个清单项(198 个可跟踪测试 + 4 条说明性备注),覆盖 web / mobile / desktop。

逐项跟踪

每项可标记 Pass / Fail / Block / N/A(再次点击取消),并可展开备注记录缺陷链接或环境。

查看进度

实时的堆叠进度条 + 每张卡片的 x/y done运行标签(如 v0.4 · macOS)用于命名本轮测试。

筛选

按文本/标签搜索,并可按平台、功能、层(Form/Func)、状态、仅 DI 阻塞项隐藏已完成过滤。

共享一轮结果

导出 / 导入 JSON 用于备份或在多台机器间迁移,Copy summary 生成 markdown 的通过/失败报告。
markdown 清单始终是唯一事实来源。编辑清单后重新运行 python3 test/test_cases/build_tracker.py 即可 —— 已保存的状态以 (平台, 功能, 文本) 的稳定哈希为键,因此重排顺序或改标签都不会丢;只有改动某一项的文本才会重置该项。

增 / 改 / 删一个用例

用例就是普通 markdown。编辑对应的 test/test_cases/<platform>/<function>/checklist.md,然后重新生成跟踪器:
python3 test/test_cases/build_tracker.py   # 重写 tracker.html
  • 新增 —— 在正确的标题下(## Form tests## Functional tests## Platform-specific)加一行:
    - [ ] 这条用例验证什么。 — [Form · Widget]
    
    第一个标签必须是 FormFunc —— 这是它被当作可跟踪测试的依据;没有该标签的行会渲染成灰色备注,且不计入进度。组件需要先改造成可注入时,补上 (DI: X)
  • 修改 —— 直接改这一行。只改标签(如 FormFunc、加 (DI))会保留已保存的状态;改文本会生成新 id,于是该项重置为未测。
  • 删除 —— 删掉这一行。它原来的状态会无害地残留在浏览器的 localStorage 里;忽略即可,或用跟踪器的 Reset
  • 新增功能或平台 —— 新建 …/<platform>/<function>/checklist.md。生成器会自动发现任意 */*/checklist.md,无需改代码即可识别;为排序好看,把名字加到 build_tracker.pyPLATFORM_ORDER / FUNCTION_ORDER,并补到上面的目录表里。

运行测试

表单层(快,不需要服务器):
cd rakullapp_core
flutter pub get
flutter analyze
flutter test
功能层(可选 —— 先起服务栈,再针对它运行带标签的测试):
# 终端 1
scripts/run_all_servers.sh
# 终端 2
flutter test --tags functional \
  --dart-define=USER_MANAGER_API_BASE=http://localhost:8010 \
  --dart-define=STUDY_API_BASE=http://localhost:8012 \
  --dart-define=COLLECTION_API_BASE=http://localhost:8013
在终端 1 按 Ctrl-C 停止服务栈。若是无人值守 / CI 运行,可后台启动、等健康检查通过、跑完再按端口停止:
bash scripts/run_all_servers.sh &
until curl -sf http://localhost:8010/health >/dev/null; do sleep 1; done
flutter test --tags functional \
  --dart-define=USER_MANAGER_API_BASE=http://localhost:8010 \
  --dart-define=STUDY_API_BASE=http://localhost:8012 \
  --dart-define=COLLECTION_API_BASE=http://localhost:8013
bash scripts/stop_all_servers.sh   # 释放 8010–8015 端口
E2E 层(可选 —— 需要 integration_test 包):
flutter test integration_test

可测性阻塞项

下列组件自己 new 了依赖,因此暂时无法用 mock 做表单测试。加上构造函数注入(就像 ArticleListViewReaderScreen 和各 *Api 类已经做的那样)即可解锁清单里的 (DI) 项。
组件阻塞点修法
LoginScreen / ConnectAppsScreenStatenew RegisterService()增加可选的 registerService 参数
DashboardScreen内部创建 StudyApi/UserApi/AuthStore通过构造函数注入这三个客户端
StudyLoginScreenStatenew UserApi()增加可选的 userApi 参数
AdminScreen 各 tab每个 tab 各自创建 UserApi/StudyApi向各 tab 注入共享客户端
TaskProgressDialogStatenew SseClient()showTaskProgress 增加可选的 sseClient 参数
VideoUploadScreenFilePicker.platform 静态单例用可注入接口封装文件选择
SentenceAudioButtonStatenew AudioPlayer()用可注入接口封装播放
RegisterService / SupabaseBootstrap顶层 http.post/http.get接受 http.Client,让 finalize/config 调用可被 mock
若日后需要团队共享、带历史记录与自动结果汇入的跟踪,这套清单可以平滑迁移到托管工具(如 Qase 或自托管的 Kiwi TCMS):让表单层产出 JUnit XML 即可 —— flutter test --machine | tojunit -o report.xml