水印相机前端

This commit is contained in:
2026-09-28 09:59:43 +08:00
parent 187a7e0911
commit c5b3d81d07
19 changed files with 2761 additions and 3 deletions
+167
View File
@@ -0,0 +1,167 @@
import 'dart:io';
import 'package:dio/dio.dart';
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:qiangbao_accounting/api/geo_api.dart';
import 'package:qiangbao_accounting/api/photo_api.dart';
import 'package:qiangbao_accounting/pages/watermark/watermark_camera_page.dart';
import 'package:qiangbao_accounting/pages/watermark/watermark_edit_page.dart';
/// 水印相机的页面布局回归测试。
///
/// 价值在之前抓过真 bug:记账首页顶部一行在 393 宽的常规手机上溢出 32px,
/// 光看代码看不出来。
///
/// 这里只断言「不溢出、不抛异常」,具体样式靠人眼验收。
///
/// 两个测试上的坑(踩过):
/// 1. 页面上的加载圈是**无限动画**,`pumpAndSettle` 永远等不到静止 → 超时。
/// 所以编辑页只能手动 pump。
/// 2. 读文件/解码图片是**真实异步 I/O**,得用 `tester.runAsync` 才跑得完。
/// 假定位:不发网络请求,直接返回一份完整快照
class _FakeGeoApi extends GeoApi {
@override
Future<GeoSnapshot> resolve() async => const GeoSnapshot(
latitude: 25.041234,
longitude: 102.712345,
address: '云南省昆明市五华区莲花池正街某小区某栋某单元',
weather: '晴',
temperature: '25℃',
);
}
/// 定位失败:页面仍应可用,只是少了地点那一行
class _FailingGeoApi extends GeoApi {
@override
Future<GeoSnapshot> resolve() async => const GeoSnapshot(error: '没有定位权限');
}
/// 假上传:立刻成功,避免测试里真的打网络
class _FakePhotoApi extends PhotoApi {
_FakePhotoApi() : super(Dio());
@override
Future<int?> upload({
required String filePath,
double? latitude,
double? longitude,
String? address,
String? weather,
String? remark,
DateTime? takenTime,
}) async =>
1;
}
void main() {
const sizes = [
('iPhone SE 窄屏', Size(320, 568)),
('常见安卓机', Size(393, 873)),
('大屏', Size(480, 1000)),
];
void useSize(WidgetTester tester, Size size) {
tester.view.physicalSize = size * 3;
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.reset);
}
/// 造一张真实的小图(编辑页要读它的文件头拿尺寸)
File makePhoto(WidgetTester tester) {
final dir = Directory.systemTemp.createTempSync('wm_layout_test');
addTearDown(() {
// Image.file 可能还占着文件句柄,删不掉就算了,别让清理把测试搞挂
try {
dir.deleteSync(recursive: true);
} catch (_) {}
});
return File('${dir.path}/photo.png')
..writeAsBytesSync(img.encodePng(img.Image(width: 120, height: 90)));
}
/// 等真实 I/O 跑完再渲染一帧
Future<void> settleIo(WidgetTester tester) async {
await tester.runAsync(() async {
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
await tester.pump();
}
for (final (label, size) in sizes) {
testWidgets('入口页 $label 不溢出', (tester) async {
useSize(tester, size);
await tester.pumpWidget(
const ProviderScope(child: MaterialApp(home: WatermarkCameraPage())),
);
await tester.pumpAndSettle();
expect(find.text('水印相机'), findsOneWidget);
expect(find.text('拍照'), findsOneWidget);
expect(find.text('从相册选'), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('编辑页 $label 不溢出', (tester) async {
useSize(tester, size);
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);
expect(find.text('加水印'), findsOneWidget);
expect(find.text('保存'), findsOneWidget);
// 假定位的地址应该显示出来了(顺带验证异步链路是通的)
expect(find.textContaining('昆明市'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
testWidgets('设置弹层在窄屏也不溢出', (tester) async {
useSize(tester, const Size(320, 568));
await tester.pumpWidget(
const ProviderScope(child: MaterialApp(home: WatermarkCameraPage())),
);
await tester.pumpAndSettle();
// 点右上角进设置
await tester.tap(find.byIcon(Icons.tune));
await tester.pumpAndSettle();
expect(find.text('水印设置'), findsOneWidget);
expect(find.text('天气温度'), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('定位失败时编辑页仍可用(只是少一行地点)', (tester) async {
useSize(tester, const Size(393, 873));
final photo = makePhoto(tester);
await tester.pumpWidget(
ProviderScope(
overrides: [
geoApiProvider.overrideWithValue(_FailingGeoApi()),
photoApiProvider.overrideWithValue(_FakePhotoApi()),
],
child: MaterialApp(home: WatermarkEditPage(imagePath: photo.path)),
),
);
await settleIo(tester);
expect(find.textContaining('没有定位权限'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}