683e19bdc1
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>
209 lines
6.5 KiB
Dart
209 lines
6.5 KiB
Dart
/// 封面/宣传图裁剪页 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,
|
||
);
|
||
});
|
||
}
|