/// 封面/宣传图裁剪页 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 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( 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 = []; 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( find.ancestor(of: done, matching: find.byType(FilledButton)), ) .enabled, isFalse, ); bogus.deleteSync(); }); testWidgets('GIF 源 → 预检判不支持(防包内 png 兜底解码挂起)且不 pop', (tester) async { final popped = []; 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( find.ancestor(of: done, matching: find.byType(FilledButton)), ) .enabled, isFalse, ); }); }