import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:qiangbao_accounting/api/note_api.dart'; import 'package:qiangbao_accounting/models/note.dart'; import 'package:qiangbao_accounting/providers/note_provider.dart'; /// 记录每次 list() 的调用,用来断言「有没有真的重新请求」。 class FakeNoteApi extends NoteApi { FakeNoteApi(this.pages) : super(Dio()); /// 依次返回的结果:第 1 次调用返回 pages[0],第 2 次返回 pages[1]…… /// 模拟「服务端数据被外部改过」的场景。 final List pages; int listCalls = 0; int lastRequestedPage = 0; @override Future list({ int page = 1, int size = 20, String scope = 'all', int? folderId, String sort = 'updated', String? keyword, String? tag, }) async { listCalls++; lastRequestedPage = page; final i = (listCalls - 1).clamp(0, pages.length - 1); return pages[i]; } @override Future> tags() async => const []; @override Future folderTree() async => FolderTree.empty; } NotePage pageOf(List ids) => NotePage( items: ids .map((id) => NoteListItem( id: id, folderId: null, title: '笔记 $id', summary: '', tags: const [], pinned: false, updateTime: '2026-09-24 10:00', )) .toList(), total: ids.length, hasMore: false, ); /// 回归测试:**外部(Web 后台 / 别的设备)改过笔记后,App 必须能自己发现**。 /// /// 背景(真实踩过的 bug,和 AI 写账单那次同源): /// `noteListProvider` 是 keepAlive 的 —— 为了从详情页返回时保住搜索词和 /// 标签筛选。代价是**再次进入页面不会重新请求**,于是你在 Web 后台导入的 /// 笔记,手机上永远看不到(表现是「还是那 2 条」)。 /// /// 这类「绕过 App 的写操作」没有任何通知可听,只能在恰当的时机主动对账。 /// 这三个入口就是那三个时机,任何一个是坏的都会让用户以为「导入失败了」。 void main() { (ProviderContainer, FakeNoteApi) boot(List pages) { final api = FakeNoteApi(pages); final container = ProviderContainer( overrides: [noteApiProvider.overrideWithValue(api)], ); addTearDown(container.dispose); return (container, api); } test('首次进入页面会加载', () async { final (container, api) = boot([pageOf([1, 2])]); container.listen(noteListProvider, (_, _) {}); // build() 里是 Future.microtask,等它跑完 await Future.delayed(Duration.zero); expect(api.listCalls, 1); expect(container.read(noteListProvider).items.length, 2); }); test('refreshOnEnter:重新进入页面能拿到外部新增的笔记', () async { // 第 1 次进页面 2 条;在 Web 上导入了 1 篇之后,第 2 次应该是 3 条 final (container, api) = boot([ pageOf([1, 2]), pageOf([3, 1, 2]), ]); container.listen(noteListProvider, (_, _) {}); await Future.delayed(Duration.zero); expect(container.read(noteListProvider).items.length, 2); // 模拟「退出页面 → 在 Web 导入 → 再次进入页面」 await container.read(noteListProvider.notifier).refreshOnEnter(); expect(api.listCalls, 2, reason: '进页面必须重新请求,不能吃缓存'); expect(container.read(noteListProvider).items.length, 3, reason: 'Web 导入的那篇必须出现'); }); test('refreshOnEnter 保留筛选条件(不能把搜索词和标签弄丢)', () async { final (container, api) = boot([pageOf([1, 2]), pageOf([1, 2])]); container.listen(noteListProvider, (_, _) {}); await Future.delayed(Duration.zero); // 用户搜了「MySQL」、选了「运维」标签、切到按标题排序 final n = container.read(noteListProvider.notifier); n.setKeyword('MySQL'); n.setTag('运维'); n.setSort('title'); await Future.delayed(const Duration(milliseconds: 400)); // 等搜索防抖 final before = container.read(noteListProvider); await n.refreshOnEnter(); final after = container.read(noteListProvider); expect(after.keyword, before.keyword, reason: '搜索词要还在'); expect(after.tag, before.tag, reason: '标签筛选要还在'); expect(after.sort, before.sort, reason: '排序方式要还在'); }); test('refreshIfStale:刚加载过就不重复请求(避免频繁进出页面白刷)', () async { final (container, api) = boot([pageOf([1, 2])]); container.listen(noteListProvider, (_, _) {}); await Future.delayed(Duration.zero); expect(api.listCalls, 1); // 刚加载完,此时切回前台不该再打一次 await container.read(noteListProvider.notifier).refreshIfStale(); expect(api.listCalls, 1, reason: '数据还新鲜,不该白打请求'); }); test('刷新是静默的:不置 loading,界面不会闪加载圈', () async { final (container, api) = boot([pageOf([1, 2]), pageOf([3, 1, 2])]); container.listen(noteListProvider, (_, _) {}); await Future.delayed(Duration.zero); // 记录刷新过程中的 loading 变化 final seen = []; final sub = container.listen(noteListProvider, (_, next) { seen.add(next.loading); }); addTearDown(sub.close); await container.read(noteListProvider.notifier).refreshOnEnter(); expect(seen.contains(true), isFalse, reason: '静默刷新不该出现 loading=true,否则列表会闪一下'); expect(container.read(noteListProvider).items.length, 3); }); }