import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:smtias_capture/models/preview_region.dart'; import 'package:smtias_capture/widgets/oriented_preview.dart'; import 'package:smtias_capture/widgets/region_crop.dart'; /// プレビューの「向き変換」「クロップ」の見た目をゴールデン画像で固定する回帰テスト. /// /// リファクタリング前に正解画像を生成・コミットし,リファクタリング後に /// `flutter test` で再照合する(1px でも変われば失敗).これは「変換を壊して /// いないか」の高速な自動チェックであり,「向きが本当に正しいか」の最終確認は /// 実機の全体スクリーンショットと基準画像(local_reference/ 配下,個人情報のため /// 非コミット)の目視比較で行う.詳細は SPEC_04 参照. void main() { // ゴールデンの基準サイズ(縦長プレビューを模す). const size = Size(240, 360); // 中央寄りのクロップ領域(表示後座標 0.0–1.0). const region = PreviewRegion(left: 0.25, top: 0.2, right: 0.75, bottom: 0.8); testWidgets('OrientedPreview: 向き変換が正解画像と一致する', (tester) async { await _pump(tester, size, const OrientedPreview(child: _TestPattern())); await expectLater( find.byKey(_targetKey), matchesGoldenFile('goldens/oriented_preview.png'), ); }); testWidgets('RegionCrop: クロップ拡大が正解画像と一致する', (tester) async { await _pump( tester, size, const RegionCrop(region: region, child: _TestPattern()), ); await expectLater( find.byKey(_targetKey), matchesGoldenFile('goldens/region_crop.png'), ); }); testWidgets('向き変換 + クロップ: パイプライン全体が正解画像と一致する', (tester) async { await _pump( tester, size, const RegionCrop( region: region, child: OrientedPreview(child: _TestPattern()), ), ); await expectLater( find.byKey(_targetKey), matchesGoldenFile('goldens/preview_pipeline.png'), ); }); } const _targetKey = ValueKey('golden-target'); /// [child] を固定サイズの [RepaintBoundary] で包んでレンダリングする. Future _pump(WidgetTester tester, Size size, Widget child) async { await tester.pumpWidget( Center( child: RepaintBoundary( key: _targetKey, child: SizedBox.fromSize(size: size, child: child), ), ), ); } /// 上下左右が一目で判別できる非対称テストパターン(向き・クロップ確認用の入力). /// /// 上=赤 / 下=青 / 左=緑 / 右=黄 の色帯と,左上寄りのマゼンタ円を描く. /// 180°回転すれば色帯の上下・左右が入れ替わり,円が右下へ移動するため, /// 反転や座標の取り違えがゴールデン差分として確実に現れる. class _TestPattern extends StatelessWidget { const _TestPattern(); @override Widget build(BuildContext context) { return CustomPaint(painter: _TestPatternPainter(), size: Size.infinite); } } class _TestPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final w = size.width; final h = size.height; final t = (w < h ? w : h) * 0.15; void fill(Rect rect, Color color) { canvas.drawRect(rect, Paint()..color = color); } fill(Offset.zero & size, const Color(0xFFFFFFFF)); // 背景:白 fill(Rect.fromLTWH(0, 0, w, t), const Color(0xFFE53935)); // 上:赤 fill(Rect.fromLTWH(0, h - t, w, t), const Color(0xFF1E88E5)); // 下:青 fill(Rect.fromLTWH(0, 0, t, h), const Color(0xFF43A047)); // 左:緑 fill(Rect.fromLTWH(w - t, 0, t, h), const Color(0xFFFDD835)); // 右:黄 // 左上寄りのマーカー円(両軸反転とクロップ中心を判別する). canvas.drawCircle( Offset(w * 0.3, h * 0.3), t * 0.6, Paint()..color = const Color(0xFFD500F9), ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }