新增:媒体上传客户端——MediaUploader 编排状态机 + 预签名直传 + 进度组件,修正 media 端点误挂 community 服务(T3-13)
CI / flutter-gates (push) Successful in 2m30s
CI / flutter-gates (push) Successful in 2m30s
- MediaUploader:选图→压缩(80→60 降质阶梯,10 MiB 超限拒绝)→createUpload →预签名 PUT 直传(进度回调)→confirm→ready assetId 交付;并发槽位 2、 顺序即 position、单图失败不拖垮整批;凭据过期预检与存储侧 403 各自动 换新凭据一次;失败重试复用压缩产物、从 createUpload 全新开始 - 孤儿防护:未 confirm 的 assetId 只以管线局部变量存在,快照 assetId 与 ready 态断言绑定,buildAttachRequests 非全员 ready 即抛 StateError - 线路修正:media 两步上传端点由 user 服务(:8082 MediaController)提供, T3-12 误挂 community 客户端(:8084 无 media 路由),补 mediaApi 分端口 直连,compose 真链路实测证实 - UploadProgressOverlay 四态进度层(05 号规范 §3.3);直传层本地 HttpServer 三线路测试(200/403/断连);compose 冒烟测试 (PATBOND_MEDIA_SMOKE=1 启用,默认跳过) - 新依赖:image_picker、flutter_image_compress(03 号评估选型) - flutter test 379 全绿(基线 347,+32)、analyze 0、format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
||||
import 'package:patbond_flutter/core/widgets/upload_progress_overlay.dart';
|
||||
import 'package:patbond_flutter/features/community/media_uploader.dart';
|
||||
|
||||
/// UploadProgressOverlay(05 号规范 §3.3):排队 / 上传中 / 失败三态
|
||||
/// 形态与失败整格点按重试。
|
||||
void main() {
|
||||
Widget host(Widget overlay) => MaterialApp(
|
||||
home: Scaffold(body: SizedBox(width: 100, height: 100, child: overlay)),
|
||||
);
|
||||
|
||||
testWidgets('排队态:scrim + 「等待中」胶囊', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(const UploadProgressOverlay(phase: MediaItemPhase.queued)),
|
||||
);
|
||||
|
||||
expect(find.text('等待中'), findsOneWidget);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('上传中态:环形进度带 value + 百分比胶囊', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
const UploadProgressOverlay(
|
||||
phase: MediaItemPhase.uploading,
|
||||
progress: 0.4,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final indicator = tester.widget<CircularProgressIndicator>(
|
||||
find.byType(CircularProgressIndicator),
|
||||
);
|
||||
expect(indicator.value, 0.4);
|
||||
expect(find.text('40%'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('confirming 定格 100%', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
const UploadProgressOverlay(
|
||||
phase: MediaItemPhase.confirming,
|
||||
progress: 0.4,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('100%'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('失败态:errorDark 图标 + 重试通栏,整格点按触发 onRetry', (tester) async {
|
||||
var retried = 0;
|
||||
await tester.pumpWidget(
|
||||
host(
|
||||
UploadProgressOverlay(
|
||||
phase: MediaItemPhase.failed,
|
||||
onRetry: () => retried++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final icon = tester.widget<Icon>(find.byIcon(Icons.error_outline));
|
||||
expect(icon.color, AppColors.errorDark);
|
||||
expect(find.text('重试'), findsOneWidget);
|
||||
// 整格(含图标区)点按即重试。
|
||||
await tester.tap(find.byIcon(Icons.error_outline));
|
||||
expect(retried, 1);
|
||||
});
|
||||
|
||||
testWidgets('失败终态(onRetry null):不显示重试通栏', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(const UploadProgressOverlay(phase: MediaItemPhase.failed)),
|
||||
);
|
||||
|
||||
expect(find.text('重试'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('成功态:150ms 淡出且不拦截点击、无残留文案', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
host(const UploadProgressOverlay(phase: MediaItemPhase.ready)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('等待中'), findsNothing);
|
||||
expect(find.text('重试'), findsNothing);
|
||||
expect(find.byType(IgnorePointer), findsWidgets);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user