1+ import 'dart:async' ;
12import 'dart:math' as math;
23import 'dart:typed_data' ;
34import 'dart:ui' as ui;
@@ -6,9 +7,26 @@ import 'package:flutter/material.dart';
67import 'package:flutter/rendering.dart' ;
78import 'package:flutter_test/flutter_test.dart' ;
89
9- import '../models/golden_flow_config.dart' ;
10+ import '../config/golden_flow_config.dart' ;
11+ import '../helpers/logger.dart' ;
12+
13+ class GoldenScreenshot {
14+ final _screenshots = < Uint8List > [];
15+
16+ void add (Uint8List screenshot) {
17+ if (screenshot.isEmpty) {
18+ logError ('[flows][GoldenScreenshot] Empty screenshot added' );
19+
20+ return ;
21+ }
22+ _screenshots.add (screenshot);
23+ logDebug (
24+ '[flows][GoldenScreenshot] Screenshot added, total count: ${_screenshots .length }' ,
25+ );
26+ }
27+
28+ List <Uint8List > get screenshots => _screenshots;
1029
11- extension Flows on WidgetTester {
1230 Future <Uint8List > captureScreenshot () async {
1331 final RenderRepaintBoundary boundary = find
1432 .byElementPredicate (
@@ -18,64 +36,140 @@ extension Flows on WidgetTester {
1836 .first
1937 .renderObject as RenderRepaintBoundary ;
2038
21- if (boundary == null ) {
22- throw Exception (
23- 'No RepaintBoundary found. Wrap your widget with RepaintBoundary.' ,
24- );
25- }
39+ logDebug ('[flows][captureScreenshot] Capturing screenshot...' );
2640
27- debugPrint ( 'Capturing screenshot...' );
41+ final ui. Image image = boundary. toImageSync ( );
2842
29- final ui.Image image = await boundary.toImage (
30- pixelRatio: view.devicePixelRatio,
43+ logDebug (
44+ '[flows][captureScreenshot] Screenshot captured, converting to bytes...' ,
45+ );
46+
47+ final ui.Image croppedImage = await _cropImage (
48+ image,
49+ Offset .zero,
50+ Size (
51+ boundary.size.width,
52+ boundary.size.height,
53+ ),
3154 );
3255
33- debugPrint ('Screenshot captured, converting to bytes...' );
56+ logDebug ('[flows][captureScreenshot] Screenshot cropped to size: '
57+ '${croppedImage .width }x${croppedImage .height }' );
58+
59+ final ByteData ? byteData = await croppedImage
60+ .toByteData (
61+ format: ui.ImageByteFormat .png,
62+ )
63+ .timeout (
64+ const Duration (seconds: 2 ),
65+ );
66+
67+ image.dispose ();
68+
69+ if (byteData == null ) {
70+ throw Exception ('Failed to get byte data from image' );
71+ }
72+
73+ logDebug ('[flows][captureScreenshot] Screenshot converted to bytes.' );
74+
75+ return byteData.buffer.asUint8List ();
76+ }
77+
78+ Future <ui.Image > _cropImage (
79+ ui.Image image,
80+ Offset topLeft,
81+ Size size,
82+ ) async {
83+ final top = topLeft.dy.round ();
84+ final left = topLeft.dx.round ();
85+ final width = size.width.round ();
86+ final height = size.height.round ();
87+
88+ logDebug ('[flows][_cropImage] Cropping image: '
89+ 'top: $top , left: $left , width: $width , height: $height ' );
3490
35- final ByteData ? byteData = await image.toByteData (
36- format: ui.ImageByteFormat .png,
91+ final byteData = await image.toByteData (format: ui.ImageByteFormat .rawRgba);
92+ if (byteData == null ) {
93+ throw Exception ('Failed to get byte data from image' );
94+ }
95+
96+ const bytesPerPixel = 4 ;
97+
98+ final originalBytes = byteData.buffer.asUint8List ();
99+ final originalWidth = image.width;
100+
101+ final croppedBytes = Uint8List (width * height * bytesPerPixel);
102+
103+ logDebug ('[flows][_cropImage] Creating cropped bytes buffer: '
104+ '${croppedBytes .length } bytes' );
105+
106+ for (int row = 0 ; row < height; row++ ) {
107+ final srcStart = ((top + row) * originalWidth + left) * bytesPerPixel;
108+ final destStart = row * width * bytesPerPixel;
109+ croppedBytes.setRange (
110+ destStart,
111+ destStart + width * bytesPerPixel,
112+ originalBytes,
113+ srcStart,
114+ );
115+ }
116+
117+ logDebug ('[flows][_cropImage] Cropped bytes created successfully.' );
118+
119+ final completer = Completer <ui.Image >();
120+
121+ ui.decodeImageFromPixels (
122+ croppedBytes,
123+ width,
124+ height,
125+ ui.PixelFormat .rgba8888,
126+ completer.complete as ui.ImageDecoderCallback ,
37127 );
38128
39- debugPrint ( 'Screenshot converted to bytes .' );
129+ logDebug ( '[flows][_cropImage] Decoding cropped image from pixels.. .' );
40130
41- return byteData ! .buffer. asUint8List () ;
131+ return completer.future ;
42132 }
43133
44134 Future <Uint8List > combineScreenshots (
45135 List <Uint8List > screenshots,
46136 GoldenFlowConfig config,
47137 List <String > stepNames,
48138 ) async {
49- debugPrint (
50- 'Starting combineScreenshots with ${screenshots .length } screenshots' );
139+ logDebug (
140+ '[flows][combineScreenshots] Starting combineScreenshots with ${screenshots .length } screenshots' ,
141+ );
51142
52143 if (screenshots.isEmpty) {
144+ logError ('No screenshots provided to combine' );
53145 throw ArgumentError ('No screenshots to combine' );
54146 }
55147
56148 try {
57- // Decodificar la primera imagen para obtener dimensiones
58- debugPrint ('Decoding first image to get dimensions...' );
149+ logDebug (
150+ '[flows][combineScreenshots] Decoding first image to get dimensions...' ,
151+ );
59152 final firstImage = await _decodeImage (screenshots.first);
153+
60154 final screenWidth = firstImage.width.toDouble ();
61155 final screenHeight = firstImage.height.toDouble ();
62156
63- debugPrint ('Screen dimensions: ${screenWidth }x${screenHeight }' );
157+ logDebug (
158+ '[flows][combineScreenshots] Screen dimensions: ${screenWidth }x$screenHeight ' ,
159+ );
64160
65- // Calcular dimensiones del canvas final
66161 final canvasDimensions = _calculateCanvasDimensions (
67162 screenshots.length,
68163 screenWidth,
69164 screenHeight,
70165 config,
71166 );
72167
73- debugPrint (
74- 'Canvas dimensions: ${canvasDimensions .width }x${canvasDimensions .height }' );
168+ logDebug (
169+ '[flows][combineScreenshots] Canvas dimensions: ${canvasDimensions .width }x${canvasDimensions .height }' ,
170+ );
75171
76- // Crear el canvas con un límite de tamaño razonable
77- final maxCanvasSize =
78- 4096 ; // Límite de tamaño para evitar problemas de memoria
172+ const maxCanvasSize = 4096 ;
79173 final scaleFactor = canvasDimensions.width > maxCanvasSize ||
80174 canvasDimensions.height > maxCanvasSize
81175 ? maxCanvasSize /
@@ -85,23 +179,24 @@ extension Flows on WidgetTester {
85179 final finalWidth = (canvasDimensions.width * scaleFactor).toInt ();
86180 final finalHeight = (canvasDimensions.height * scaleFactor).toInt ();
87181
88- debugPrint (
89- 'Final canvas size: ${finalWidth }x${finalHeight } (scale: $scaleFactor )' );
182+ logDebug (
183+ '[flows][combineScreenshots] Final canvas size: ${finalWidth }x$finalHeight (scale: $scaleFactor )' ,
184+ );
90185
91186 final recorder = ui.PictureRecorder ();
92187 final canvas = Canvas (recorder);
93188
94- // Fondo blanco
95189 canvas.drawRect (
96190 Rect .fromLTWH (0 , 0 , finalWidth.toDouble (), finalHeight.toDouble ()),
97191 Paint ()..color = Colors .white,
98192 );
99193
100- debugPrint ( ' Drawing screenshots on canvas...' );
194+ logDebug ( '[flows][combineScreenshots] Drawing screenshots on canvas...' );
101195
102- // Procesar cada screenshot
103196 for (int i = 0 ; i < screenshots.length; i++ ) {
104- debugPrint ('Processing screenshot ${i + 1 }/${screenshots .length }' );
197+ logDebug (
198+ '[flows][combineScreenshots] Processing screenshot ${i + 1 }/${screenshots .length }' ,
199+ );
105200
106201 try {
107202 final image = await _decodeImage (screenshots[i]);
@@ -112,9 +207,12 @@ extension Flows on WidgetTester {
112207 config,
113208 );
114209
115- // Dibujar la imagen escalada
116210 final srcRect = Rect .fromLTWH (
117- 0 , 0 , image.width.toDouble (), image.height.toDouble ());
211+ 0 ,
212+ 0 ,
213+ image.width.toDouble (),
214+ image.height.toDouble (),
215+ );
118216 final dstRect = Rect .fromLTWH (
119217 position.dx,
120218 position.dy,
@@ -124,7 +222,6 @@ extension Flows on WidgetTester {
124222
125223 canvas.drawImageRect (image, srcRect, dstRect, Paint ());
126224
127- // Dibujar el título del paso
128225 _drawStepTitle (
129226 canvas,
130227 stepNames[i],
@@ -134,57 +231,60 @@ extension Flows on WidgetTester {
134231 scaleFactor,
135232 );
136233
137- // Dibujar borde
138234 _drawBorder (
139235 canvas,
140236 position,
141237 screenWidth * scaleFactor,
142238 screenHeight * scaleFactor,
143239 );
144240
145- // Liberar memoria de la imagen
146241 image.dispose ();
147242 } catch (e) {
148- debugPrint ('Error processing screenshot $i : $e ' );
243+ logError (
244+ '[flows][combineScreenshots] Error processing screenshot $i : $e ' ,
245+ );
149246 rethrow ;
150247 }
151248 }
152249
153- debugPrint ( ' Converting canvas to image...' );
250+ logDebug ( '[flows][combineScreenshots] Converting canvas to image...' );
154251
155- // Convertir a imagen con manejo de errores
156252 final picture = recorder.endRecording ();
157253 final finalImage = await picture.toImage (finalWidth, finalHeight);
158254
159- debugPrint ( ' Converting image to bytes...' );
255+ logDebug ( '[flows][combineScreenshots] Converting image to bytes...' );
160256
161257 final byteData = await finalImage.toByteData (
162258 format: ui.ImageByteFormat .png,
163259 );
164260
165- // Limpiar recursos
166261 finalImage.dispose ();
167262 picture.dispose ();
168263
169264 if (byteData == null ) {
265+ logError (
266+ '[flows][combineScreenshots] Failed to convert image to bytes' );
170267 throw Exception ('Failed to convert image to bytes' );
171268 }
172269
173270 final result = byteData.buffer.asUint8List ();
174- debugPrint (
175- '✓ Successfully combined screenshots. Final size: ${result .length } bytes' );
271+ logDebug (
272+ '[flows][combineScreenshots] ✓ Successfully combined screenshots. Final size: ${result .length } bytes' ,
273+ );
176274
177275 return result;
178276 } catch (e) {
179- debugPrint ('Error in combineScreenshots: $e ' );
180- debugPrint ('Stack trace: ${StackTrace .current }' );
277+ logError ('[flows][combineScreenshots] Error in combineScreenshots: $e ' );
278+ logError (
279+ '[flows][combineScreenshots] Stack trace: ${StackTrace .current }' );
181280 rethrow ;
182281 }
183282 }
184283
185284 Future <ui.Image > _decodeImage (Uint8List bytes) async {
186285 final codec = await ui.instantiateImageCodec (bytes);
187286 final frame = await codec.getNextFrame ();
287+
188288 return frame.image;
189289 }
190290
@@ -227,7 +327,7 @@ extension Flows on WidgetTester {
227327 GoldenFlowConfig config,
228328 ) {
229329 const titleHeight = 40.0 ;
230- const padding = 10 .0 ;
330+ const padding = 20 .0 ;
231331
232332 switch (config.layoutType) {
233333 case FlowLayoutType .vertical:
0 commit comments