水印优化+ORC优化

This commit is contained in:
2026-09-28 11:39:19 +08:00
parent c5b3d81d07
commit 6176c49964
19 changed files with 1701 additions and 171 deletions
+137 -4
View File
@@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:qiangbao_accounting/api/geo_api.dart';
import 'package:qiangbao_accounting/api/photo_api.dart';
import 'package:qiangbao_accounting/core/storage/local_storage.dart';
import 'package:qiangbao_accounting/models/photo.dart';
import 'package:qiangbao_accounting/pages/watermark/watermark_camera_page.dart';
import 'package:qiangbao_accounting/pages/watermark/watermark_edit_page.dart';
@@ -41,9 +44,12 @@ class _FailingGeoApi extends GeoApi {
Future<GeoSnapshot> resolve() async => const GeoSnapshot(error: '没有定位权限');
}
/// 假上传:立刻成功,避免测试里真的打网络
/// 假照片接口:不真打网络。默认返回空列表 —— 入口页会在 initState 里拉列表,
/// 不给它替身的话测试会真的去连 localhost,每个用例白等好几秒。
class _FakePhotoApi extends PhotoApi {
_FakePhotoApi() : super(Dio());
_FakePhotoApi({this.photos = const []}) : super(Dio());
final List<PhotoItem> photos;
@override
Future<int?> upload({
@@ -56,6 +62,17 @@ class _FakePhotoApi extends PhotoApi {
DateTime? takenTime,
}) async =>
1;
@override
Future<PhotoPage> list({int page = 1, int size = 30}) async =>
PhotoPage(items: photos, total: photos.length, hasMore: false);
@override
Future<String> downloadOriginal(
String url, {
void Function(int received, int total)? onProgress,
}) async =>
'';
}
void main() {
@@ -93,11 +110,22 @@ void main() {
await tester.pump();
}
/// 弹出带输入框的弹层后不能用 pumpAndSettle:
/// TextField 的光标闪烁是无限动画,永远等不到静止。手动推两帧就够。
Future<void> openSheet(WidgetTester tester, String buttonText) async {
await tester.tap(find.text(buttonText));
await tester.pump(); // 开始入场动画
await tester.pump(const Duration(milliseconds: 400)); // 动画走完
}
for (final (label, size) in sizes) {
testWidgets('入口页 $label 不溢出', (tester) async {
useSize(tester, size);
await tester.pumpWidget(
const ProviderScope(child: MaterialApp(home: WatermarkCameraPage())),
ProviderScope(
overrides: [photoApiProvider.overrideWithValue(_FakePhotoApi())],
child: const MaterialApp(home: WatermarkCameraPage()),
),
);
await tester.pumpAndSettle();
@@ -133,7 +161,10 @@ void main() {
testWidgets('设置弹层在窄屏也不溢出', (tester) async {
useSize(tester, const Size(320, 568));
await tester.pumpWidget(
const ProviderScope(child: MaterialApp(home: WatermarkCameraPage())),
ProviderScope(
overrides: [photoApiProvider.overrideWithValue(_FakePhotoApi())],
child: const MaterialApp(home: WatermarkCameraPage()),
),
);
await tester.pumpAndSettle();
@@ -146,6 +177,108 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('已上传的照片在入口页以网格显示', (tester) async {
useSize(tester, const Size(393, 873));
const photos = [
PhotoItem(
id: 1,
url: '/uploads/photo/a.jpg',
thumbUrl: '/uploads/photo/a.thumb.jpg',
takenAt: '2026-09-28 14:30',
address: '昆明市五华区',
weather: '晴 25℃',
remark: '',
latitude: 25.0,
longitude: 102.7,
),
PhotoItem(
id: 2,
url: '/uploads/photo/b.jpg',
// 缩略图没生成出来的情况:应该退回用原图,而不是空一块
thumbUrl: null,
takenAt: '2026-09-27 09:00',
address: '',
weather: '',
remark: '',
latitude: null,
longitude: null,
),
];
await tester.pumpWidget(
ProviderScope(
overrides: [
photoApiProvider.overrideWithValue(_FakePhotoApi(photos: photos)),
],
child: const MaterialApp(home: WatermarkCameraPage()),
),
);
await tester.pumpAndSettle();
expect(find.text('最近拍摄'), findsOneWidget);
expect(find.text('共 2 张'), findsOneWidget);
expect(find.byType(GridView), findsOneWidget);
// 测试环境加载不到网络图,会落到 errorBuilder 的占位图标;数量仍然对
expect(find.byType(Image), findsNWidgets(2));
expect(tester.takeException(), isNull);
});
testWidgets('编辑内容弹层在窄屏也不溢出(时间 + 常用地址 + 四个输入框)', (tester) async {
useSize(tester, const Size(320, 568));
final photo = makePhoto(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
geoApiProvider.overrideWithValue(_FakeGeoApi()),
photoApiProvider.overrideWithValue(_FakePhotoApi()),
],
child: MaterialApp(home: WatermarkEditPage(imagePath: photo.path)),
),
);
await settleIo(tester);
await openSheet(tester, '编辑内容');
expect(find.text('时间'), findsOneWidget);
expect(find.text('常用地址'), findsOneWidget);
expect(find.text('备注'), findsOneWidget);
expect(tester.takeException(), isNull);
});
// 放在最后:它会初始化 LocalStorage 单例(单例的 _prefs 只取第一次)
testWidgets('常用地址:点一下填进地址框', (tester) async {
useSize(tester, const Size(393, 873));
SharedPreferences.setMockInitialValues({
'watermark_quick_addresses': <String>['昆明市公安局', '昆明南站'],
});
await LocalStorage.instance.init();
final photo = makePhoto(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
geoApiProvider.overrideWithValue(_FakeGeoApi()),
photoApiProvider.overrideWithValue(_FakePhotoApi()),
],
child: MaterialApp(home: WatermarkEditPage(imagePath: photo.path)),
),
);
await settleIo(tester);
await openSheet(tester, '编辑内容');
expect(find.text('昆明市公安局'), findsOneWidget);
await tester.tap(find.text('昆明市公安局'));
await tester.pump();
// 弹层里第一个输入框是「地址」,点快捷地址应该把它填上
final addressField = tester.widget<TextField>(find.byType(TextField).at(0));
expect(addressField.controller?.text, '昆明市公安局');
expect(tester.takeException(), isNull);
});
testWidgets('定位失败时编辑页仍可用(只是少一行地点)', (tester) async {
useSize(tester, const Size(393, 873));
final photo = makePhoto(tester);