feat(mobile): UI-03 capture and upload pages (13-16)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/// 连拍页 widget 测试(P3 计划 Task 9,R17 连拍入队 / O01 现场反馈)。
|
||||
/// 连拍页 widget 测试(UI-03 页面 13 深色摄影界面;R17 连拍入队 / O01)。
|
||||
///
|
||||
/// 打桩面:FakeCaptureSource(capture 返回真实 JPEG 临时文件,无相机
|
||||
/// 平台通道)+ 内存 TaskStore + 临时目录准备器 + 非 Android 权限门替身
|
||||
/// (静默跳过通道)。覆盖:快门即拍即入队、连拍串行、分类切换仅影响
|
||||
/// 之后拍摄、多选补充入队、待传/失败徽章实时刷新、有待传任务退出确认、
|
||||
/// 权限拒绝/初始化失败视图与重试。
|
||||
/// 平台通道;可注入闪光灯能力/记录翻转释放)+ 内存 TaskStore + 临时目录
|
||||
/// 准备器 + 非 Android 权限门替身。覆盖:快门即拍即入队、连拍串行、分类
|
||||
/// 切换仅影响之后拍摄、多选补充入队、待传/失败徽章实时刷新、有待传任务
|
||||
/// 退出确认、权限拒绝/初始化失败视图与重试、深色界面(#10161E)、快门
|
||||
/// ≥72dp、闪光灯按能力显示、前后摄切换重建、最近一张缩略图。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
@@ -26,6 +27,7 @@ import 'package:photo_live/features/uploads/task_state.dart';
|
||||
import 'package:photo_live/features/uploads/task_store.dart';
|
||||
import 'package:photo_live/l10n/app_localizations.dart';
|
||||
import 'package:photo_live/pages/capture_page.dart';
|
||||
import 'package:photo_live/ui/tokens.dart';
|
||||
|
||||
import '../helpers/fakes.dart';
|
||||
|
||||
@@ -33,8 +35,9 @@ const List<int> kJpgBytes = <int>[
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00,
|
||||
];
|
||||
|
||||
/// 拍摄源替身:capture 依次弹出预置文件;initialize 可注入失败次数。
|
||||
class FakeCaptureSource implements CaptureSource {
|
||||
/// 拍摄源替身:capture 依次弹出预置文件;initialize 可注入失败次数;
|
||||
/// 闪光灯能力可注入(记录开关调用);释放计数供前后切换断言。
|
||||
class FakeCaptureSource extends CaptureSource {
|
||||
FakeCaptureSource(this._files);
|
||||
|
||||
final List<File> _files;
|
||||
@@ -42,6 +45,13 @@ class FakeCaptureSource implements CaptureSource {
|
||||
int failNextInits = 0;
|
||||
Object? initError;
|
||||
|
||||
/// 闪光灯能力(默认不支持——UI 不显示开关)。
|
||||
bool flashSupported = false;
|
||||
final List<bool> flashCalls = <bool>[];
|
||||
|
||||
/// dispose 计数(前后摄切换释放断言)。
|
||||
int disposeCalls = 0;
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
initializeCalls++;
|
||||
@@ -61,7 +71,14 @@ class FakeCaptureSource implements CaptureSource {
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
bool get supportsFlash => flashSupported;
|
||||
|
||||
@override
|
||||
Future<void> setFlashEnabled(bool enabled) async =>
|
||||
flashCalls.add(enabled);
|
||||
|
||||
@override
|
||||
Future<void> dispose() async => disposeCalls++;
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -71,6 +88,9 @@ void main() {
|
||||
late FakeCaptureSource source;
|
||||
late List<String> galleryPaths;
|
||||
|
||||
/// 工厂调用记录(前后摄切换断言)。
|
||||
late List<bool> factoryPreferFront;
|
||||
|
||||
final categories = <Category>[
|
||||
const Category(
|
||||
id: 'cat-1',
|
||||
@@ -106,11 +126,18 @@ void main() {
|
||||
final shot3 = File('${tmp.path}/IMG_0003.jpg')..writeAsBytesSync(kJpgBytes);
|
||||
source = FakeCaptureSource([shot1, shot2, shot3]);
|
||||
galleryPaths = <String>[];
|
||||
factoryPreferFront = <bool>[];
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await store.close();
|
||||
await tmp.delete(recursive: true);
|
||||
try {
|
||||
await tmp.delete(recursive: true);
|
||||
} on Object {
|
||||
// Windows:底部缩略图 FileImage 的异步读取可能仍持有临时文件句柄
|
||||
//(fake-async 区内真实 IO 完成回调不被冲刷)——删除失败不判定
|
||||
// 业务断言失败,残留目录交由系统临时目录清理。
|
||||
}
|
||||
});
|
||||
|
||||
CaptureTarget target() => CaptureTarget(
|
||||
@@ -134,8 +161,12 @@ void main() {
|
||||
(_) => throw StateError('连拍页不应发起 API 请求')),
|
||||
),
|
||||
),
|
||||
captureSourceFactoryProvider
|
||||
.overrideWith((ref) => () async => source),
|
||||
captureSourceFactoryProvider.overrideWith(
|
||||
(ref) => ({bool preferFront = false}) async {
|
||||
factoryPreferFront.add(preferFront);
|
||||
return source;
|
||||
},
|
||||
),
|
||||
uploadPreparerProvider.overrideWith(
|
||||
(ref) => UploadPreparer(
|
||||
store,
|
||||
@@ -188,10 +219,33 @@ void main() {
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('快门即拍即入队:任务落库 queued + 待传徽章 1', (tester) async {
|
||||
testWidgets('深色摄影界面:页面背景 #10161E(不随主题)', (tester) async {
|
||||
await pumpCapture(tester);
|
||||
expect(
|
||||
tester.widget<Scaffold>(find.byType(Scaffold)).backgroundColor,
|
||||
kColorPhotoBackdrop,
|
||||
reason: '§3 相机界面固定深色背景',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('快门 ≥72dp(§4 页面 13)+ 相机控制齐备', (tester) async {
|
||||
await pumpCapture(tester);
|
||||
|
||||
final shutterSize = tester.getSize(find.byKey(const ValueKey('capture-shutter')));
|
||||
expect(shutterSize.width, greaterThanOrEqualTo(72));
|
||||
expect(shutterSize.height, greaterThanOrEqualTo(72));
|
||||
|
||||
// 前后摄切换 + 相册入口(缩略图占位)常驻;闪光灯未注入能力不出现。
|
||||
expect(find.byIcon(Icons.flip_camera_ios_outlined), findsOneWidget);
|
||||
expect(find.byIcon(Icons.photo_library_outlined), findsOneWidget);
|
||||
expect(find.byIcon(Icons.flash_off), findsNothing,
|
||||
reason: '源不支持闪光灯时不显示开关');
|
||||
});
|
||||
|
||||
testWidgets('快门即拍即入队:任务落库 queued + 顶栏待传徽章 1', (tester) async {
|
||||
await pumpCapture(tester);
|
||||
expect(find.text('PREVIEW_MARK'), findsOneWidget, reason: '相机预览就绪');
|
||||
expect(find.text('Queued 0'), findsOneWidget);
|
||||
expect(find.text('0 queued'), findsOneWidget);
|
||||
|
||||
await shoot(tester);
|
||||
|
||||
@@ -202,7 +256,21 @@ void main() {
|
||||
expect(tasks.single.albumId, 'alb-1');
|
||||
expect(tasks.single.categoryId, isNull, reason: '默认分类');
|
||||
expect(tasks.single.filename, 'IMG_0001.jpg');
|
||||
expect(find.text('Queued 1'), findsOneWidget, reason: '徽章实时刷新');
|
||||
expect(find.text('1 queued'), findsOneWidget, reason: '徽章实时刷新');
|
||||
});
|
||||
|
||||
testWidgets('最近一张缩略图:拍摄前为相册占位,拍摄后显示照片',
|
||||
(tester) async {
|
||||
await pumpCapture(tester);
|
||||
expect(find.byType(Image), findsNothing, reason: '拍摄前无缩略图');
|
||||
|
||||
await shoot(tester);
|
||||
|
||||
expect(
|
||||
find.byType(Image),
|
||||
findsOneWidget,
|
||||
reason: '拍摄后底部显示最近一张(缩略图位)',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('入队成功反馈:短暂浮层出现(不逐张弹确认,§4.5)',
|
||||
@@ -247,7 +315,36 @@ void main() {
|
||||
'IMG_0001.jpg',
|
||||
'IMG_0002.jpg',
|
||||
]));
|
||||
expect(find.text('Queued 2'), findsOneWidget);
|
||||
expect(find.text('2 queued'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('闪光灯:源支持才显示;点击开→关记录调用', (tester) async {
|
||||
source.flashSupported = true;
|
||||
await pumpCapture(tester);
|
||||
|
||||
expect(find.byIcon(Icons.flash_off), findsOneWidget, reason: '默认关');
|
||||
await tester.tap(find.byIcon(Icons.flash_off));
|
||||
await tester.pump();
|
||||
|
||||
expect(source.flashCalls, [true]);
|
||||
expect(find.byIcon(Icons.flash_on), findsOneWidget, reason: '开启后图标翻转');
|
||||
});
|
||||
|
||||
testWidgets('前后摄切换:释放当前源 → 反向重建(工厂收 preferFront)',
|
||||
(tester) async {
|
||||
await pumpCapture(tester);
|
||||
expect(factoryPreferFront, [false]);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.flip_camera_ios_outlined));
|
||||
await tester.pump();
|
||||
var guard = 0;
|
||||
while (find.text('PREVIEW_MARK').evaluate().isEmpty && guard++ < 20) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
expect(source.disposeCalls, 1, reason: '旧源已释放(相机生命周期)');
|
||||
expect(factoryPreferFront, [false, true], reason: '反向重建');
|
||||
expect(find.text('PREVIEW_MARK'), findsOneWidget, reason: '预览恢复');
|
||||
});
|
||||
|
||||
testWidgets('分类切换:hint 提示 + 仅影响之后拍摄(A15 归属不变)',
|
||||
|
||||
Reference in New Issue
Block a user