Files
photo-live/apps/mobile/test/pages/image_crop_page_test.dart
T
浅念 683e19bdc1 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>
2026-09-28 16:01:24 +08:00

209 lines
6.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 封面/宣传图裁剪页 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,
);
});
}