Skip to content

Commit 07b6423

Browse files
Merge pull request #14 from bancolombia/feature/flows
add multiple screen support [WIP]
2 parents d060bea + d807a34 commit 07b6423

32 files changed

+1234
-150
lines changed

analysis_options.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ dart_code_linter:
100100
- no-boolean-literal-compare
101101
- no-empty-block # Quitarla puede ser una opcion
102102
- no-equal-then-else
103-
- no-magic-number
103+
- no-magic-number:
104+
severity: none
104105
- prefer-trailing-comma: #S/N​
105106
severity: none
106107
- prefer-conditional-expressions

example/main.dart renamed to example/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'package:flutter/material.dart';
22

3-
import 'lib/src/presentation/core/app_widget.dart';
3+
import 'src/presentation/core/app_widget.dart';
44

55
void main() {
66
runApp(const AppWidget());
34.3 KB
Loading
92 KB
Loading
74.9 KB
Loading
71.9 KB
Loading
76.5 KB
Loading

example/test/src/presentation/home/home_page_golden_test.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,27 @@ void main() {
4343
),
4444
FlowStep(
4545
stepName: 'home2',
46-
widgetBuilder: () => const ButtonWidget(),
46+
widgetBuilder: () => const HomePage(
47+
title: 'Page 1',
48+
),
49+
),
50+
FlowStep(
51+
stepName: 'home2',
52+
widgetBuilder: () => const HomePage(
53+
title: 'Page 2',
54+
),
55+
),
56+
FlowStep(
57+
stepName: 'home2',
58+
widgetBuilder: () => const HomePage(
59+
title: 'Page 3',
60+
),
4761
),
4862
],
49-
const GoldenFlowConfig(testName: 'multiple_screens'),
63+
GoldenFlowConfig(
64+
testName: 'multiple_screens',
65+
device: iPhone13,
66+
spacing: 100,
67+
),
5068
);
5169
}

lib/bc_golden_plugin.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
library bc_golden_plugin;
22

3-
export 'src/bc_golden_configuration.dart';
4-
export 'src/golden_device_data.dart';
5-
export 'src/golden_testing_tools.dart';
6-
export 'src/models/golden_flow_config.dart';
3+
export 'src/config/bc_golden_configuration.dart';
4+
export 'src/config/golden_device_data.dart';
5+
export 'src/config/golden_flow_config.dart';
6+
export 'src/testkit/golden_testing_tools.dart';

lib/src/helpers/flows.dart renamed to lib/src/capture/golden_screenshot.dart

Lines changed: 147 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:math' as math;
23
import 'dart:typed_data';
34
import 'dart:ui' as ui;
@@ -6,9 +7,26 @@ import 'package:flutter/material.dart';
67
import 'package:flutter/rendering.dart';
78
import '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

Comments
 (0)