feat(app): crop cover & banner before upload (crop_your_image, 16:9 / 3:1)
review fixes: drop GIF from pre-sniff (package maps gif→png decode → never-ready hang); PopScope blocks back during processing Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/// 封面/宣传图裁剪页 widget 测试(crop_your_image 回调式 API)。
|
||||
///
|
||||
/// 真实异步边界(源文件读取 + 包内 parse/crop 的 compute + 编码落盘
|
||||
/// Isolate.run)在 testWidgets 的 FakeAsync 下不会自行推进——参照
|
||||
/// crop_your_image 自身测试的驱动方式:tap + 等待都包进 tester.runAsync,
|
||||
/// 用真实 Future.delayed 等 isolate 完成。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:photo_live/l10n/app_localizations.dart';
|
||||
import 'package:photo_live/pages/image_crop_page.dart';
|
||||
|
||||
void main() {
|
||||
late final Directory tmp;
|
||||
late final String fixtureJpg;
|
||||
late final String fixtureGif;
|
||||
|
||||
setUpAll(() {
|
||||
tmp = Directory.systemTemp.createTempSync('image_crop_page_test');
|
||||
// 320x240(4:3)JPG 夹具:运行时生成,避免仓库二进制资产。
|
||||
final source = img.Image(width: 320, height: 240, numChannels: 3);
|
||||
img.fill(source, color: img.ColorRgb8(80, 120, 200));
|
||||
fixtureJpg = '${tmp.path}${Platform.pathSeparator}fixture.jpg';
|
||||
File(fixtureJpg).writeAsBytesSync(img.encodeJpg(source, quality: 90));
|
||||
// GIF 夹具:包内 formatDetector 兜底按 png 解码必然失败挂起,
|
||||
// 页面预检须直接判不支持(防 never-ready)。
|
||||
fixtureGif = '${tmp.path}${Platform.pathSeparator}fixture.gif';
|
||||
File(fixtureGif).writeAsBytesSync(img.encodeGif(source));
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
/// 打开裁剪页(zh 本地化);onPopped 接住 pop 结果。
|
||||
Future<void> openCropPage(
|
||||
WidgetTester tester,
|
||||
String sourcePath, {
|
||||
required double aspectRatio,
|
||||
int? maxOutputWidth,
|
||||
void Function(String? value)? onPopped,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('zh'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => Center(
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
final value = await Navigator.of(context).push<String>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ImageCropPage(
|
||||
sourcePath: sourcePath,
|
||||
aspectRatio: aspectRatio,
|
||||
maxOutputWidth: maxOutputWidth,
|
||||
),
|
||||
),
|
||||
);
|
||||
onPopped?.call(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
// 真实异步必须「等→pump→再等→再 pump」:读取完成后经 pump 才构建
|
||||
// Crop(parse compute 此刻才启动),需再等真实时间让 isolate 回复落地。
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
await tester.pump();
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('取消 → pop(null)', (tester) async {
|
||||
String? popped = 'sentinel';
|
||||
await openCropPage(
|
||||
tester,
|
||||
fixtureJpg,
|
||||
aspectRatio: 16 / 9,
|
||||
onPopped: (value) => popped = value,
|
||||
);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.text('取消'));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(popped, isNull);
|
||||
});
|
||||
|
||||
testWidgets('确认 → pop 出存在的 .jpg 路径', (tester) async {
|
||||
String? popped;
|
||||
await openCropPage(
|
||||
tester,
|
||||
fixtureJpg,
|
||||
aspectRatio: 16 / 9,
|
||||
maxOutputWidth: 1920,
|
||||
onPopped: (value) => popped = value,
|
||||
);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.text('完成'));
|
||||
await tester.pump();
|
||||
// crop(compute)→ 编码落盘(Isolate.run)→ pop。
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(popped, isNotNull);
|
||||
expect(popped!.endsWith('.jpg'), isTrue);
|
||||
expect(File(popped!).existsSync(), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('确认 → 输出尊重 maxOutputWidth 且保持纵横比', (tester) async {
|
||||
String? popped;
|
||||
await openCropPage(
|
||||
tester,
|
||||
fixtureJpg,
|
||||
aspectRatio: 16 / 9,
|
||||
maxOutputWidth: 160,
|
||||
onPopped: (value) => popped = value,
|
||||
);
|
||||
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.text('完成'));
|
||||
await tester.pump();
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
await tester.pump();
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(popped, isNotNull);
|
||||
final decoded = img.decodeImage(File(popped!).readAsBytesSync());
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.width, 160);
|
||||
expect(decoded.height, 90);
|
||||
});
|
||||
|
||||
testWidgets('非图片源 → 提示不支持且不 pop', (tester) async {
|
||||
final bogus = File('${tmp.path}${Platform.pathSeparator}bogus.jpg');
|
||||
bogus.writeAsStringSync('definitely not an image');
|
||||
|
||||
final popped = <String?>[];
|
||||
await openCropPage(
|
||||
tester,
|
||||
bogus.path,
|
||||
aspectRatio: 3 / 1,
|
||||
onPopped: popped.add,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('暂不支持该图片格式,请选择 JPG/PNG'), findsOneWidget);
|
||||
expect(popped, isEmpty);
|
||||
// 完成不可用(无裁剪可做)。
|
||||
final done = find.text('完成');
|
||||
expect(
|
||||
tester
|
||||
.widget<FilledButton>(
|
||||
find.ancestor(of: done, matching: find.byType(FilledButton)),
|
||||
)
|
||||
.enabled,
|
||||
isFalse,
|
||||
);
|
||||
|
||||
bogus.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('GIF 源 → 预检判不支持(防包内 png 兜底解码挂起)且不 pop',
|
||||
(tester) async {
|
||||
final popped = <String?>[];
|
||||
await openCropPage(
|
||||
tester,
|
||||
fixtureGif,
|
||||
aspectRatio: 16 / 9,
|
||||
onPopped: popped.add,
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('暂不支持该图片格式,请选择 JPG/PNG'), findsOneWidget);
|
||||
expect(popped, isEmpty);
|
||||
// 完成不可用:包内若按 png 兜底解析 GIF,将永远到不了 ready。
|
||||
final done = find.text('完成');
|
||||
expect(
|
||||
tester
|
||||
.widget<FilledButton>(
|
||||
find.ancestor(of: done, matching: find.byType(FilledButton)),
|
||||
)
|
||||
.enabled,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user