This commit is contained in:
2026-09-03 17:50:23 +08:00
parent 7bfafc7be3
commit 77d3aad6fc
45 changed files with 1398 additions and 1088 deletions
+126 -53
View File
@@ -120,33 +120,33 @@ void main() {
await tester.tap(finder);
}
testWidgets('未下载:显示使用按钮,点击后下载完成自动变为使用', (tester) async {
testWidgets('未下载:显示下载按钮,点击后下载完成自动变为使用', (tester) async {
final m = manager([_item()]);
await tester.runAsync(() => m.refresh());
await pumpSection(tester, m);
expect(find.text('数据集A'), findsOneWidget);
expect(find.text('使用'), findsOneWidget);
expect(find.text('下载'), findsOneWidget);
await tapVisible(tester, find.text('使用'));
await tapVisible(tester, find.text('下载'));
await tester.pump(); // 下载启动,进度条出现
// 驱动下载 + 自动激活的 IO 链走完,直至 UI 呈现「使用」
await pumpUntilFound(tester, find.text('使用'));
// 驱动下载 + 自动激活的 IO 链走完,直至 UI 呈现「使用
await pumpUntilFound(tester, find.text('使用'));
await tester.pumpAndSettle();
expect(m.isDownloaded(7, kVariantS), isTrue);
expect(m.isActive(7, kVariantS), isTrue);
expect(find.text('使用'), findsOneWidget);
expect(find.text('使用'), findsOneWidget);
});
testWidgets('已激活:再次点击取消使用', (tester) async {
testWidgets('已激活:点击使用中取消使用', (tester) async {
final m = manager([_item()]);
await tester.runAsync(() => m.refresh());
await tester.runAsync(() => m.downloadModel(m.catalog.first));
await pumpSection(tester, m);
expect(find.text('使用'), findsOneWidget);
await tapVisible(tester, find.text('使用'));
expect(find.text('使用'), findsOneWidget);
await tapVisible(tester, find.text('使用'));
// 排空 setActive 的激活集重载与落盘 IO,直至 UI 呈现「使用」
await pumpUntilFound(tester, find.text('使用'));
await tester.pumpAndSettle();
@@ -169,7 +169,7 @@ void main() {
expect(find.text('暂无已发布模型'), findsOneWidget);
});
testWidgets('下载中:显示进度和取消按钮,取消后恢复使用', (tester) async {
testWidgets('下载中:显示进度和取消按钮,取消后恢复下载', (tester) async {
final m = ModelManager(
baseUrl: 'http://test.local',
client: _StreamingClient(),
@@ -178,83 +178,156 @@ void main() {
await tester.runAsync(() => m.refresh());
await pumpSection(tester, m);
await tapVisible(tester, find.text('使用'));
await tapVisible(tester, find.text('下载'));
// 首个分块到达,进度条与取消按钮出现(IO 链需交替驱动)
await pumpUntilFound(tester, find.text('取消'),
fake: const Duration(milliseconds: 400));
expect(find.text('取消'), findsOneWidget);
await tapVisible(tester, find.text('取消'));
// 推进 fake 时钟触发延迟分块 → 取消分支清场(.part 删除等真实 IO),直至恢复「使用
await pumpUntilFound(tester, find.text('使用'),
// 推进 fake 时钟触发延迟分块 → 取消分支清场(.part 删除等真实 IO),直至恢复「下载
await pumpUntilFound(tester, find.text('下载'),
fake: const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(m.isDownloaded(7, kVariantS), isFalse);
expect(m.isActive(7, kVariantS), isFalse);
expect(find.text('使用'), findsOneWidget);
expect(find.text('下载'), findsOneWidget);
});
testWidgets('双档位:s/n 两卡独立,n 档下载备好后切档即用', (tester) async {
final m =
manager([_item(), _item(variant: kVariantN, name: '数据集B')]);
testWidgets('双档位:同物种合并一张卡,下载只启用目标档,使用即停旧档', (tester) async {
final m = manager([_item(), _item(variant: kVariantN)]);
await tester.runAsync(() => m.refresh());
await pumpSection(tester, m);
// 分段控件默认 s;两卡并排,s 卡按钮「使用」,非当前档 n 卡按钮「下载」
expect(find.text('s 高识别'), findsOneWidget);
// 合并卡:同一数据集只一张卡,无双卡/无档位角标;双档行标注两档
expect(find.text('数据集A'), findsOneWidget);
expect(find.text('数据集B'), findsOneWidget);
expect(find.text('s'), findsOneWidget, reason: '档位角标');
expect(find.text('n'), findsOneWidget, reason: '档位角标');
expect(find.text('使用'), findsOneWidget);
expect(find.text('下载'), findsOneWidget, reason: '非当前档入口为「下载」');
expect(find.text('下载'), findsOneWidget, reason: '合并卡只有一个主按钮');
expect(find.textContaining('高精度 v1.0.0'), findsOneWidget);
expect(find.textContaining('高性能 v1.0.0'), findsOneWidget);
// 先激活 s 档
await tapVisible(tester, find.text('使用'));
await pumpUntilFound(tester, find.text('已使用'));
expect(m.isActive(7, kVariantS), isTrue);
// 下载 n 档:完成后自动备好(n 非当前档),显示「已备好」而非「已使用」
// 一次下载取回双档:同物种只启用目标档(默认高精度 s),n 档备好不启用。
// 双档下载收尾链交错,等 models 真正重载完成再断言
await tapVisible(tester, find.text('下载'));
await pumpUntilFound(tester, find.text('已备好'));
await tester.pumpAndSettle();
expect(m.isActive(7, kVariantN), isTrue);
expect(m.mode, kVariantS, reason: '下载非当前档不应改变识别档位');
expect(m.models.length, 1);
expect(m.models.first.variant, kVariantS, reason: '当前仍只加载 s 档');
// 点「已备好」切换档位 → n 档立即生效(已使用),s 档转为备好态
await tapVisible(tester, find.text('已备好'));
// 「已使用」切换前后都在场(s 使用 → n 使用),须等 manager 状态真正切档
await pumpUntilFound(tester, find.text('使用中'));
for (var i = 0;
i < 50 &&
!(m.mode == kVariantN &&
m.models.isNotEmpty &&
m.models.first.variant == kVariantN);
!(m.isDownloaded(7, kVariantS) &&
m.isDownloaded(7, kVariantN) &&
m.isActive(7, kVariantS) &&
!m.isActive(7, kVariantN) &&
m.models.length == 1 &&
m.models.first.variant == kVariantS);
i++) {
await pumpRealIo(tester);
}
await tester.pumpAndSettle();
expect(m.mode, kVariantN);
expect(m.isDownloaded(7, kVariantS), isTrue);
expect(m.isDownloaded(7, kVariantN), isTrue);
expect(m.isActive(7, kVariantS), isTrue);
expect(m.isActive(7, kVariantN), isFalse,
reason: '同物种同时只运行目标档,另一档备好不叠加启用');
expect(m.mode, kVariantS, reason: '下载双档不应改变目标档');
expect(m.models.length, 1);
expect(m.models.first.variant, kVariantN, reason: '切档后热加载 n 档模型');
expect(find.text('使用'), findsOneWidget, reason: 'n 档卡已使用');
expect(find.text('已备好'), findsOneWidget, reason: 's 档转为备好态');
expect(m.models.first.variant, kVariantS);
expect(find.text('使用'), findsOneWidget);
// 目标档切到高性能:运行中的 s 不受影响;n 已下载未启用 → 主按钮「使用」
// (保存目标档偏好是真实 IO,等按钮呈现才算 UI 落定;2026-09-03 简化:
// 不再叫「改用X」、不显示「当前使用 X」提示)
await tapVisible(tester, find.text('高性能'));
await pumpUntilFound(tester, find.text('使用'));
await tester.pumpAndSettle();
expect(m.mode, kVariantN);
expect(m.isActive(7, kVariantS), isTrue,
reason: '切换目标档不应改变运行中的模型');
expect(find.text('使用'), findsOneWidget);
expect(find.textContaining('当前使用'), findsNothing);
// 点「使用」:同物种 s 自动停用,n 热启用(setActive 尾部的落盘
// 与 notify 是真实 IO,等 UI 呈现「使用中」才算整链落定)
await tapVisible(tester, find.text('使用'));
await pumpUntilFound(tester, find.text('使用中'));
await tester.pumpAndSettle();
expect(m.isActive(7, kVariantS), isFalse, reason: '启用目标档自动停用另一档');
expect(m.isActive(7, kVariantN), isTrue);
expect(m.models.length, 1);
expect(m.models.first.variant, kVariantN);
expect(find.text('使用中'), findsOneWidget, reason: '主按钮跟随当前档');
});
testWidgets('识别模式分段控件:点击切档生效', (tester) async {
final m = manager([_item(), _item(variant: kVariantN, name: '数据集A')]);
testWidgets('不同动物可用不同档位:使用只作用于点击的动物', (tester) async {
final m = manager([
_item(datasetId: 7, name: '数据集A'),
_item(datasetId: 7, variant: kVariantN, name: '数据集A'),
_item(datasetId: 8, name: '数据集B'),
_item(datasetId: 8, variant: kVariantN, name: '数据集B'),
]);
await tester.runAsync(() => m.refresh());
// 两个物种都下载并运行默认档(高精度 s),各自 n 档备好
for (final it in m.catalog) {
await tester.runAsync(() => m.downloadModel(it));
}
await pumpSection(tester, m);
await tapVisible(tester, find.text('n 高性能'));
for (var i = 0; i < 50 && m.mode != kVariantN; i++) {
expect(m.models.length, 2);
expect(find.text('使用中'), findsNWidgets(2));
// 目标档切高性能:两卡主按钮都变「使用」(各自 s 仍在运行、n 已备好)
await tapVisible(tester, find.text('高性能'));
await pumpUntilFound(tester, find.text('使用'));
expect(find.text('使用'), findsNWidgets(2));
// 只点第一张卡(数据集 7,目录序在前):该动物切到高性能,另一动物保持高精度
await tapVisible(tester, find.text('使用').first);
await pumpUntilFound(tester, find.text('使用中')); // 数据集 7 的卡先翻为使用中
for (var i = 0;
i < 50 &&
!(m.models.length == 2 &&
m.models.firstWhere((b) => b.datasetId == 7).variant ==
kVariantN &&
m.models.firstWhere((b) => b.datasetId == 8).variant ==
kVariantS);
i++) {
await pumpRealIo(tester);
}
await tester.pumpAndSettle();
expect(m.mode, kVariantN, reason: '点击分段控件应切档');
// 档位持久化(重启恢复)由 model_manager_test「识别档位持久化」覆盖;
expect(m.isActive(7, kVariantN), isTrue);
expect(m.isActive(7, kVariantS), isFalse, reason: '启用目标档自动停用同动物另一档');
expect(m.isActive(8, kVariantS), isTrue, reason: '未点击的动物保持原档位');
expect(m.isActive(8, kVariantN), isFalse);
expect(m.models.length, 2, reason: '两个动物仍同时识别');
expect(m.models.firstWhere((b) => b.datasetId == 7).variant, kVariantN);
expect(m.models.firstWhere((b) => b.datasetId == 8).variant, kVariantS);
expect(find.text('使用中'), findsOneWidget, reason: '只有数据集 7 的卡使用中');
expect(find.text('使用'), findsOneWidget,
reason: '数据集 8 目标档已备好:按钮就是使用(不叫改用)');
expect(find.textContaining('当前使用'), findsNothing);
});
testWidgets('识别模式分段控件:只改目标档偏好,不影响已在运行的模型', (tester) async {
final m = manager([_item(), _item(variant: kVariantN)]);
await tester.runAsync(() => m.refresh());
await tester.runAsync(() => m.downloadModel(m.catalog.first)); // s 启用
await pumpSection(tester, m);
expect(find.text('高精度'), findsOneWidget, reason: '分段控件选中默认档');
expect(find.text('高性能'), findsOneWidget);
expect(find.text('使用中'), findsOneWidget);
await tapVisible(tester, find.text('高性能'));
// 目标档 n 尚未下载(之前只下了 s)→ 主按钮变为「下载」:仍不切运行模型
await pumpUntilFound(tester, find.text('下载'));
await tester.pumpAndSettle();
expect(m.mode, kVariantN, reason: '点击分段控件应改目标档');
expect(m.isActive(7, kVariantS), isTrue, reason: '只改偏好不切运行模型');
expect(m.models.length, 1);
expect(m.models.first.variant, kVariantS, reason: '没有热加载/切换');
expect(find.text('下载'), findsOneWidget, reason: '卡片按钮面向新目标档');
expect(find.textContaining('当前使用'), findsNothing);
// 目标档偏好持久化由 model_manager_test「识别目标档位偏好持久化」覆盖;
// widget 测试内 tap 触发的文件写与真实 IO 交错不可控,不在本层断言重启
});
}