diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a610055..f013376 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: flutter pub get - name: Run tests - run: flutter test + run: flutter test --coverage - name: Upload golden failures if: always() uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index e6fe372..57a4a36 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ migrate_working_dir/ /pubspec.lock /coverage /.vscode/ +**/coverage/ # Web related lib/generated_plugin_registrant.dart diff --git a/README.md b/README.md index 2f6cd3b..152c16d 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,48 @@ The `GoldenCaptureConfig` class allows you to customize how multiple screenshots - `maxScreensPerRow`: Maximum screenshots per row (for grid layout) - `device`: Optional device configuration +### Animation Testing 🎬 + +For testing animations at specific timestamps, use `BcGoldenCapture.animation`: + +```dart +BcGoldenCapture.animation( + 'Button scale animation', + AnimatedButton(), + [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 150), + frameName: 'scaled', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 300), + frameName: 'end', + ), + ], + GoldenAnimationConfig( + testName: 'button_animation', + totalDuration: Duration(milliseconds: 300), + animationSteps: [...], // Same steps as above + layoutType: CaptureLayoutType.horizontal, + showTimelineLabels: true, + ), + animationSetup: (tester) async { + // Trigger the animation + await tester.tap(find.byType(AnimatedButton)); + await tester.pump(); + }, +); +``` + +The animation testing feature captures frames at specific moments in your animation timeline, creating a comprehensive visual test that shows the animation's progression. This is particularly useful for: + +- **UI Transitions**: Validating smooth transitions between states +- **Loading Animations**: Ensuring consistent spinner or progress animations + ## bcGoldenTest (Legacy) 🏗 The is a legacy function that is still supported but deprecated. For new tests, please use `BcGoldenCapture.single` instead. This function is default tagged with "golden" and also has additional features for the tests, see the code below: diff --git a/example/test/src/presentation/animations/animation_golden_test.dart b/example/test/src/presentation/animations/animation_golden_test.dart new file mode 100644 index 0000000..e623f88 --- /dev/null +++ b/example/test/src/presentation/animations/animation_golden_test.dart @@ -0,0 +1,182 @@ +import 'package:bc_golden_plugin/bc_golden_plugin.dart'; +import 'package:flutter/material.dart'; + +void main() { + // Test de ejemplo simple para demostrar la funcionalidad + BcGoldenCapture.animation( + 'Simple fade animation test', + const SimpleFadeAnimation(), + GoldenAnimationConfig( + testName: 'simple_fade', + totalDuration: const Duration(milliseconds: 500), + animationSteps: const [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'start_invisible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 290), + frameName: 'half_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 320), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 340), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 360), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 380), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 400), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 420), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 440), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 460), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 480), + frameName: 'fully_visible', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 500), + frameName: 'fully_visible', + ), + ], + layoutType: CaptureLayoutType.horizontal, + spacing: 20.0, + showTimelineLabels: true, + device: GoldenDeviceData.iPhone13, + ), + ); + + BcGoldenCapture.animation( + 'Loading spinner rotation', + const CircularProgressIndicator(), + const GoldenAnimationConfig( + testName: 'spinner_rotation', + totalDuration: Duration(milliseconds: 1000), + showTimelineLabels: true, + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: '0_degrees', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 250), + frameName: '90_degrees', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 500), + frameName: '180_degrees', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 750), + frameName: '270_degrees', + ), + ], // Mismos pasos + layoutType: CaptureLayoutType.grid, + maxScreensPerRow: 2, + ), + ); +} + +/// Widget de ejemplo simple con animación de fade +class SimpleFadeAnimation extends StatefulWidget { + const SimpleFadeAnimation({super.key}); + + @override + State createState() => _SimpleFadeAnimationState(); +} + +class _SimpleFadeAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(_controller); + + // Iniciar la animación automáticamente + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.forward(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + backgroundColor: Colors.white, + body: Center( + child: AnimatedBuilder( + animation: _fadeAnimation, + builder: (context, child) { + return Opacity( + opacity: _fadeAnimation.value, + child: Container( + width: 200, + height: 100, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: const Center( + child: Text( + 'Fade Animation', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/example/test/src/presentation/animations/goldens/simple_fade_animation.png b/example/test/src/presentation/animations/goldens/simple_fade_animation.png new file mode 100644 index 0000000..97cf6e9 Binary files /dev/null and b/example/test/src/presentation/animations/goldens/simple_fade_animation.png differ diff --git a/example/test/src/presentation/animations/goldens/spinner_rotation_animation.png b/example/test/src/presentation/animations/goldens/spinner_rotation_animation.png new file mode 100644 index 0000000..aa21021 Binary files /dev/null and b/example/test/src/presentation/animations/goldens/spinner_rotation_animation.png differ diff --git a/example/test/src/presentation/home/failures/manual_golden_isolatedDiff.png b/example/test/src/presentation/home/failures/manual_golden_isolatedDiff.png new file mode 100644 index 0000000..c04fda0 Binary files /dev/null and b/example/test/src/presentation/home/failures/manual_golden_isolatedDiff.png differ diff --git a/example/test/src/presentation/home/failures/manual_golden_maskedDiff.png b/example/test/src/presentation/home/failures/manual_golden_maskedDiff.png new file mode 100644 index 0000000..eb87366 Binary files /dev/null and b/example/test/src/presentation/home/failures/manual_golden_maskedDiff.png differ diff --git a/example/test/src/presentation/home/failures/manual_golden_masterImage.png b/example/test/src/presentation/home/failures/manual_golden_masterImage.png new file mode 100644 index 0000000..cbb3c9c Binary files /dev/null and b/example/test/src/presentation/home/failures/manual_golden_masterImage.png differ diff --git a/example/test/src/presentation/home/failures/manual_golden_testImage.png b/example/test/src/presentation/home/failures/manual_golden_testImage.png new file mode 100644 index 0000000..5dee26b Binary files /dev/null and b/example/test/src/presentation/home/failures/manual_golden_testImage.png differ diff --git a/example/test/src/presentation/home/home_page_golden_test.dart b/example/test/src/presentation/home/home_page_golden_test.dart index 9316ec3..a4d11f1 100644 --- a/example/test/src/presentation/home/home_page_golden_test.dart +++ b/example/test/src/presentation/home/home_page_golden_test.dart @@ -64,10 +64,8 @@ void main() { ), ); - testWidgets('Manual golden test', (tester) async { + BcGoldenCapture.single('Manual golden test', (tester) async { await tester.runAsync(() async { - GoldenScreenshot screenshotter = GoldenScreenshot(); - tester.configureWindow( GoldenDeviceData.iPhone13, ); @@ -81,7 +79,7 @@ void main() { await tester.pumpAndSettle(); - await screenshotter.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester.tap( find.byKey( @@ -91,9 +89,9 @@ void main() { await tester.pumpAndSettle(); - await screenshotter.captureScreenshot(); + await tester.captureGoldenScreenshot(); - final combinedScreenshot = await screenshotter.combineScreenshots( + final combinedScreenshot = await tester.combineGoldenScreenshots( GoldenCaptureConfig( testName: 'manual_golden', device: GoldenDeviceData.iPhone13, diff --git a/lib/bc_golden_plugin.dart b/lib/bc_golden_plugin.dart index c38764b..fbdc68b 100644 --- a/lib/bc_golden_plugin.dart +++ b/lib/bc_golden_plugin.dart @@ -3,10 +3,12 @@ /// This library provides configuration and testing tools for the bc_golden_plugin. library bc_golden_plugin; -export 'package:logger/src/log_level.dart'; +export 'package:logger/logger.dart'; +export 'src/capture/golden_animation_capture.dart'; export 'src/capture/golden_screenshot.dart'; export 'src/config/bc_golden_configuration.dart'; +export 'src/config/golden_animation_config.dart'; export 'src/config/golden_capture_config.dart'; export 'src/config/golden_device_data.dart'; export 'src/config/window_configuration.dart'; diff --git a/lib/src/capture/golden_animation_capture.dart b/lib/src/capture/golden_animation_capture.dart new file mode 100644 index 0000000..66baeba --- /dev/null +++ b/lib/src/capture/golden_animation_capture.dart @@ -0,0 +1,392 @@ +// ignore_for_file: avoid-ignoring-return-values, avoid-non-null-assertion + +import 'dart:math' as math; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../config/golden_animation_config.dart'; +import '../config/golden_capture_config.dart'; +import '../helpers/logger.dart'; +import 'golden_screenshot.dart'; +import 'helpers.dart'; + +/// ## GoldenAnimationCapture +/// Extension on WidgetTester for capturing golden tests of Flutter animations. +/// +/// This extension provides functionality to capture multiple frames of an animation +/// at specific timestamps, creating a comprehensive visual test that shows +/// the animation's progression over time. +/// +/// ## Usage +/// ```dart +/// await tester.captureAnimation( +/// widget: MyAnimatedWidget(), +/// config: GoldenAnimationConfig( +/// testName: 'button_bounce_animation', +/// totalDuration: Duration(milliseconds: 500), +/// animationSteps: [ +/// GoldenAnimationStep( +/// timestamp: Duration(milliseconds: 0), +/// frameName: 'start', +/// ), +/// GoldenAnimationStep( +/// timestamp: Duration(milliseconds: 250), +/// frameName: 'peak', +/// ), +/// GoldenAnimationStep( +/// timestamp: Duration(milliseconds: 500), +/// frameName: 'end', +/// ), +/// ], +/// ), +/// animationSetup: (tester) async { +/// // Optional setup code +/// }, +/// ); +/// ``` +extension GoldenAnimationCapture on WidgetTester { + /// Captures an animation by taking screenshots at specified timestamps. + /// + /// This method pumps the widget, starts the animation, and captures frames + /// at the timestamps defined in the configuration. + /// + /// * [widget] The animated widget to test. + /// * [config] Configuration containing animation steps and settings. + /// * [animationSetup] Optional function to set up the animation before starting. + /// + /// Returns the combined image containing all captured frames. + Future captureAnimation( + Widget widget, + GoldenAnimationConfig config, + Future Function(WidgetTester)? animationSetup, + ) async { + if (!config.isValid) { + throw ArgumentError( + 'Animation configuration is invalid. All steps must be within totalDuration.', + ); + } + + logDebug('[animation] Starting animation capture for: ${config.testName}'); + logDebug( + '[animation] Total duration: ${config.totalDuration.inMilliseconds}ms', + ); + logDebug('[animation] Steps count: ${config.animationSteps.length}'); + + clearGoldenScreenshots(); + + await pumpWidget(widget); + + if (animationSetup != null) { + await animationSetup(this); + } + + final sortedSteps = config.sortedSteps; + + Duration previousTimestamp = Duration.zero; + + for (int index = 0; index < sortedSteps.length; index++) { + final step = sortedSteps[index]; + final timeDiff = step.timestamp - previousTimestamp; + + logDebug( + '[animation] Capturing frame at ${step.timestamp.inMilliseconds}ms: ${step.frameName}', + ); + + // Pump to the specific timestamp + if (timeDiff > Duration.zero) { + await pump(timeDiff); + } + + // Execute setup action for this step if provided + if (step.setupAction != null) { + await step.setupAction!(this); + } + + // Capture the frame using the extension + await captureGoldenScreenshot(); + + // Execute verification action for this step if provided + if (step.verifyAction != null) { + await step.verifyAction!(this); + } + + previousTimestamp = step.timestamp; + } + + logDebug('[animation] Combining ${goldenScreenshots.length} frames'); + + // Create step names for the combined image + final stepNames = sortedSteps + .map( + (step) => '${step.frameName} - ${step.timestamp.toString()}', + ) + .toList(); + + // Combine all captured frames into a single image using the extension + final combinedImage = await combineAnimationScreenshots( + config, + stepNames, + ); + + logDebug('[animation] Animation capture completed successfully'); + + return combinedImage; + } + + Future combineAnimationScreenshots( + GoldenAnimationConfig config, + List stepNames, + ) async { + final screenshots = goldenScreenshots; + + logDebug( + '[flows][combineScreenshots] Starting combineScreenshots with ${screenshots.length} screenshots', + ); + + if (screenshots.isEmpty) { + logError('No screenshots provided to combine'); + throw ArgumentError('No screenshots to combine'); + } + + try { + logDebug( + '[flows][combineScreenshots] Decoding first image to get dimensions...', + ); + final firstImage = + await decodeImageBytes(screenshots.firstOrNull ?? Uint8List(0)); + + final screenWidth = firstImage.width.toDouble(); + final screenHeight = firstImage.height.toDouble(); + + logDebug( + '[flows][combineScreenshots] Screen dimensions: ${screenWidth}x$screenHeight', + ); + + final canvasDimensions = _calculateCanvasDimensions( + screenshots.length, + screenWidth, + screenHeight, + config, + ); + + logDebug( + '[flows][combineScreenshots] Canvas dimensions: ${canvasDimensions.width}x${canvasDimensions.height}', + ); + + const maxCanvasSize = 4096; + final scaleFactor = canvasDimensions.width > maxCanvasSize || + canvasDimensions.height > maxCanvasSize + ? maxCanvasSize / + math.max(canvasDimensions.width, canvasDimensions.height) + : 1.0; + + final finalWidth = (canvasDimensions.width * scaleFactor).toInt(); + final finalHeight = (canvasDimensions.height * scaleFactor).toInt(); + + logDebug( + '[flows][combineScreenshots] Final canvas size: ${finalWidth}x$finalHeight (scale: $scaleFactor)', + ); + + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + canvas.drawRect( + Rect.fromLTWH(0, 0, finalWidth.toDouble(), finalHeight.toDouble()), + Paint()..color = Colors.white, + ); + + logDebug( + '[flows][combineScreenshots] Drawing screenshots on canvas...', + ); + + for (int index = 0; index < screenshots.length; index++) { + logDebug( + '[flows][combineScreenshots] Processing screenshot ${index + 1}/${screenshots.length}', + ); + + try { + final image = await decodeImageBytes(screenshots[index]); + final position = _calculateImagePosition( + index, + screenWidth * scaleFactor, + screenHeight * scaleFactor, + config, + ); + + final srcRect = Rect.fromLTWH( + 0, + 0, + image.width.toDouble(), + image.height.toDouble(), + ); + final dstRect = Rect.fromLTWH( + position.dx, + position.dy, + screenWidth * scaleFactor, + screenHeight * scaleFactor, + ); + + canvas.drawImageRect(image, srcRect, dstRect, Paint()); + + _drawStepTitle( + canvas, + stepNames[index], + position, + screenWidth * scaleFactor, + screenHeight * scaleFactor, + scaleFactor, + ); + + drawBorder( + canvas, + position, + screenWidth * scaleFactor, + screenHeight * scaleFactor, + ); + + image.dispose(); + } catch (e) { + logError( + '[flows][combineScreenshots] Error processing screenshot $index: $e', + ); + rethrow; + } + } + + logDebug('[flows][combineScreenshots] Converting canvas to image...'); + + final picture = recorder.endRecording(); + final finalImage = await picture.toImage(finalWidth, finalHeight); + + logDebug('[flows][combineScreenshots] Converting image to bytes...'); + + final byteData = await finalImage.toByteData( + format: ui.ImageByteFormat.png, + ); + + finalImage.dispose(); + picture.dispose(); + + if (byteData == null) { + logError( + '[flows][combineScreenshots] Failed to convert image to bytes', + ); + throw Exception('Failed to convert image to bytes'); + } + + final result = byteData.buffer.asUint8List(); + logDebug( + '[flows][combineScreenshots] ✓ Successfully combined screenshots. Final size: ${result.length} bytes', + ); + + return result; + } catch (e) { + logError('[flows][combineScreenshots] Error in combineScreenshots: $e'); + logError( + '[flows][combineScreenshots] Stack trace: ${StackTrace.current}', + ); + rethrow; + } + } + + void _drawStepTitle( + Canvas canvas, + String title, + Offset position, + double screenWidth, + double screenHeight, + double scaleFactor, + ) { + final fontSize = math.max(10.0, 14.0 * scaleFactor); + + final textPainter = TextPainter( + text: TextSpan( + text: title, + style: TextStyle( + color: Colors.black87, + fontSize: fontSize, + fontWeight: FontWeight.w600, + fontFamily: 'Roboto-Regular', + ), + ), + textDirection: TextDirection.ltr, + ); + + textPainter.layout(maxWidth: screenWidth); + + // Background for the text + canvas.drawRect( + Rect.fromLTWH( + position.dx, + position.dy + screenHeight, + screenWidth, + 30 * scaleFactor, + ), + Paint()..color = Colors.grey.shade100, + ); + + // Text + textPainter.paint( + canvas, + Offset( + position.dx + 8 * scaleFactor, + position.dy + screenHeight + 8 * scaleFactor, + ), + ); + + textPainter.dispose(); + } + + Size _calculateCanvasDimensions( + int screenCount, + double screenWidth, + double screenHeight, + GoldenAnimationConfig config, + ) { + const titleHeight = 40.0; + const padding = 20.0; + + if (screenCount > 5) { + final rows = (screenCount / config.maxScreensPerRow).ceil(); + final cols = config.maxScreensPerRow; + + return Size( + (screenWidth + config.spacing) * cols + padding, + (screenHeight + titleHeight + config.spacing) * rows + padding, + ); + } + + return Size( + (screenWidth + config.spacing) * screenCount + padding, + screenHeight + titleHeight + (padding * 2), + ); + } + + Offset _calculateImagePosition( + int index, + double screenWidth, + double screenHeight, + GoldenCaptureConfig config, + ) { + const padding = 20.0; + const titleHeight = 40.0; + + if (index >= 5) { + final row = (index / config.maxScreensPerRow).floor(); + final col = index % config.maxScreensPerRow; + + return Offset( + padding + (screenWidth + config.spacing) * col, + padding + (screenHeight + titleHeight + config.spacing) * row, + ); + } + + return Offset( + padding + (screenWidth + config.spacing) * index, + padding, + ); + } +} diff --git a/lib/src/capture/golden_screenshot.dart b/lib/src/capture/golden_screenshot.dart index 840f7f8..d78a32e 100644 --- a/lib/src/capture/golden_screenshot.dart +++ b/lib/src/capture/golden_screenshot.dart @@ -1,38 +1,29 @@ -/// A class that handles capturing and managing golden screenshots. +/// An extension on WidgetTester that handles capturing and managing golden screenshots. /// -/// This class provides functionality to capture screenshots, add them to a collection, +/// This extension provides functionality to capture screenshots, add them to a collection, /// crop images, and combine multiple screenshots into a single image. It also includes /// methods for calculating canvas dimensions based on the layout configuration and /// positioning images on the canvas. /// /// ## Usage -/// To use the `GoldenScreenshot` class, create an instance and call its methods -/// to capture and manage screenshots as needed. +/// To use the `GoldenScreenshot` extension, call its methods directly on a WidgetTester +/// instance within your tests. /// /// ### Example /// ```dart -/// final goldenScreenshot = GoldenScreenshot(); -/// final screenshot = await goldenScreenshot.captureScreenshot(); +/// testWidgets('My golden test', (tester) async { +/// await tester.captureGoldenScreenshot(); /// -/// goldenScreenshot.add(screenshot); -/// -/// final combinedImage = await goldenScreenshot.combineScreenshots( -/// config, -/// stepNames, -/// ); +/// final combinedImage = await tester.combineGoldenScreenshots( +/// config, +/// stepNames, +/// ); +/// }); /// ``` /// /// ## Methods -/// - `add(Uint8List screenshot)`: Adds a screenshot to the collection. -/// - `List get screenshots`: Retrieves the list of captured screenshots. -/// - `Future captureScreenshot()`: Captures a screenshot of the current widget. -/// - `Future combineScreenshots(List screenshots, GoldenCaptureConfig config, List stepNames)`: Combines multiple screenshots into a single image. -/// -/// ## Private Methods -/// - `Future _cropImage(ui.Image image, Offset topLeft, Size size)`: Crops the given image to the specified size. -/// - `Future _decodeImage(Uint8List bytes)`: Decodes the image from the provided byte data. -/// - `Size _calculateCanvasDimensions(int screenCount, double screenWidth, double screenHeight, GoldenCaptureConfig config)`: Calculates the dimensions of the canvas based on the number of screens and layout configuration. -/// - `Offset _calculateImagePosition(int index, double screenWidth, double screenHeight, GoldenCaptureConfig config)`: Calculates the position of an image on the canvas based on its index and layout configuration. +/// - `Future captureGoldenScreenshot()`: Captures a screenshot of the current widget and returns it. +/// - `Future combineGoldenScreenshots(GoldenCaptureConfig config, List stepNames)`: Combines multiple screenshots into a single image. library; import 'dart:async'; @@ -46,13 +37,17 @@ import 'package:flutter_test/flutter_test.dart'; import '../config/golden_capture_config.dart'; import '../helpers/logger.dart'; +import 'helpers.dart'; -class GoldenScreenshot { - final _screenshots = []; +final Map> _screenshotStorage = {}; - List get screenshots => _screenshots; +extension GoldenScreenshot on WidgetTester { + /// Gets the list of captured screenshots for this tester instance. + List get goldenScreenshots => _screenshotStorage[this] ?? []; - Future captureScreenshot() async { + /// Captures a screenshot of the current widget and adds it to the collection. + /// Returns the captured screenshot as Uint8List. + Future captureGoldenScreenshot() async { final RenderRepaintBoundary boundary = find .byElementPredicate( (element) => element.renderObject is RenderRepaintBoundary, @@ -69,7 +64,7 @@ class GoldenScreenshot { '[flows][captureScreenshot] Screenshot captured, converting to bytes...', ); - final ui.Image croppedImage = await _cropImage( + final ui.Image croppedImage = await _cropImageToSize( image, Offset.zero, Size( @@ -97,71 +92,26 @@ class GoldenScreenshot { logDebug('[flows][captureScreenshot] Screenshot converted to bytes.'); - screenshots.add( - byteData.buffer.asUint8List(), - ); - } - - Future _cropImage( - ui.Image image, - Offset topLeft, - Size size, - ) async { - final top = topLeft.dy.round(); - final left = topLeft.dx.round(); - final width = size.width.round(); - final height = size.height.round(); - - logDebug('[flows][_cropImage] Cropping image: ' - 'top: $top, left: $left, width: $width, height: $height'); - - final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); - if (byteData == null) { - throw Exception('Failed to get byte data from image'); - } - - const bytesPerPixel = 4; - - final originalBytes = byteData.buffer.asUint8List(); - final originalWidth = image.width; - - final croppedBytes = Uint8List(width * height * bytesPerPixel); - - logDebug('[flows][_cropImage] Creating cropped bytes buffer: ' - '${croppedBytes.length} bytes'); + final screenshot = byteData.buffer.asUint8List(); - for (int row = 0; row < height; row++) { - final srcStart = ((top + row) * originalWidth + left) * bytesPerPixel; - final destStart = row * width * bytesPerPixel; - croppedBytes.setRange( - destStart, - destStart + width * bytesPerPixel, - originalBytes, - srcStart, - ); - } + _screenshotStorage[this] ??= []; + _screenshotStorage[this]!.add(screenshot); - logDebug('[flows][_cropImage] Cropped bytes created successfully.'); - - final completer = Completer(); - - ui.decodeImageFromPixels( - croppedBytes, - width, - height, - ui.PixelFormat.rgba8888, - completer.complete as ui.ImageDecoderCallback, - ); - - logDebug('[flows][_cropImage] Decoding cropped image from pixels...'); + return screenshot; + } - return completer.future; + /// Clears all captured screenshots for this tester instance. + void clearGoldenScreenshots() { + _screenshotStorage[this]?.clear(); } - Future combineScreenshots( + /// Combines multiple screenshots into a single image. + Future combineGoldenScreenshots( GoldenCaptureConfig config, List stepNames, ) async { + final screenshots = goldenScreenshots; + logDebug( '[flows][combineScreenshots] Starting combineScreenshots with ${screenshots.length} screenshots', ); @@ -176,7 +126,7 @@ class GoldenScreenshot { '[flows][combineScreenshots] Decoding first image to get dimensions...', ); final firstImage = - await _decodeImage(screenshots.firstOrNull ?? Uint8List(0)); + await decodeImageBytes(screenshots.firstOrNull ?? Uint8List(0)); final screenWidth = firstImage.width.toDouble(); final screenHeight = firstImage.height.toDouble(); @@ -228,7 +178,7 @@ class GoldenScreenshot { ); try { - final image = await _decodeImage(screenshots[index]); + final image = await decodeImageBytes(screenshots[index]); final position = _calculateImagePosition( index, screenWidth * scaleFactor, @@ -260,7 +210,7 @@ class GoldenScreenshot { scaleFactor, ); - _drawBorder( + drawBorder( canvas, position, screenWidth * scaleFactor, @@ -312,13 +262,61 @@ class GoldenScreenshot { } } - Future _decodeImage(Uint8List bytes) async { - final codec = await ui.instantiateImageCodec(bytes); - final frame = await codec.getNextFrame(); + /// Crops an image to the specified dimensions. + Future _cropImageToSize( + ui.Image image, + Offset topLeft, + Size size, + ) async { + final top = topLeft.dy.round(); + final left = topLeft.dx.round(); + final width = size.width.round(); + final height = size.height.round(); + + logDebug('[flows][_cropImage] Cropping image: ' + 'top: $top, left: $left, width: $width, height: $height'); + + final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + if (byteData == null) { + throw Exception('Failed to get byte data from image'); + } - return frame.image; + const bytesPerPixel = 4; + final originalBytes = byteData.buffer.asUint8List(); + final originalWidth = image.width; + final croppedBytes = Uint8List(width * height * bytesPerPixel); + + logDebug('[flows][_cropImage] Creating cropped bytes buffer: ' + '${croppedBytes.length} bytes'); + + for (int row = 0; row < height; row++) { + final srcStart = ((top + row) * originalWidth + left) * bytesPerPixel; + final destStart = row * width * bytesPerPixel; + croppedBytes.setRange( + destStart, + destStart + width * bytesPerPixel, + originalBytes, + srcStart, + ); + } + + logDebug('[flows][_cropImage] Cropped bytes created successfully.'); + + final completer = Completer(); + ui.decodeImageFromPixels( + croppedBytes, + width, + height, + ui.PixelFormat.rgba8888, + completer.complete as ui.ImageDecoderCallback, + ); + + logDebug('[flows][_cropImage] Decoding cropped image from pixels...'); + + return completer.future; } + /// Calculates the dimensions of the canvas based on layout configuration. Size _calculateCanvasDimensions( int screenCount, double screenWidth, @@ -351,6 +349,7 @@ class GoldenScreenshot { } } + /// Calculates the position of an image on the canvas. Offset _calculateImagePosition( int index, double screenWidth, @@ -383,6 +382,7 @@ class GoldenScreenshot { } } + /// Draws the step title on the canvas. void _drawStepTitle( Canvas canvas, String title, @@ -408,7 +408,7 @@ class GoldenScreenshot { textPainter.layout(maxWidth: screenWidth); - // Fondo para el texto + // Background for the text canvas.drawRect( Rect.fromLTWH( position.dx, @@ -419,7 +419,7 @@ class GoldenScreenshot { Paint()..color = Colors.grey.shade100, ); - // Texto + // Text textPainter.paint( canvas, Offset( @@ -430,19 +430,4 @@ class GoldenScreenshot { textPainter.dispose(); } - - void _drawBorder( - Canvas canvas, - Offset position, - double screenWidth, - double screenHeight, - ) { - canvas.drawRect( - Rect.fromLTWH(position.dx, position.dy, screenWidth, screenHeight), - Paint() - ..color = Colors.grey.shade300 - ..style = PaintingStyle.stroke - ..strokeWidth = 1, - ); - } } diff --git a/lib/src/capture/helpers.dart b/lib/src/capture/helpers.dart new file mode 100644 index 0000000..fd41402 --- /dev/null +++ b/lib/src/capture/helpers.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +Future decodeImageBytes(Uint8List bytes) async { + final codec = await ui.instantiateImageCodec(bytes); + final frame = await codec.getNextFrame(); + + return frame.image; +} + +void drawBorder( + Canvas canvas, + Offset position, + double screenWidth, + double screenHeight, +) { + canvas.drawRect( + Rect.fromLTWH(position.dx, position.dy, screenWidth, screenHeight), + Paint() + ..color = Colors.grey.shade300 + ..style = PaintingStyle.stroke + ..strokeWidth = 1, + ); +} diff --git a/lib/src/config/golden_animation_config.dart b/lib/src/config/golden_animation_config.dart new file mode 100644 index 0000000..a6b4467 --- /dev/null +++ b/lib/src/config/golden_animation_config.dart @@ -0,0 +1,82 @@ +// ignore_for_file: prefer-match-file-name +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'golden_capture_config.dart'; + +/// ## GoldenAnimationStep +/// Represents a specific frame in an animation timeline to be captured. +/// +/// This class defines when to capture a frame during an animation and allows +/// for optional setup and verification actions at that specific timestamp. +/// +/// * [timestamp] The exact time in the animation when this frame should be captured in ms. +/// * [frameName] A descriptive name for this frame (e.g., "start", "peak", "end"). +/// * [setupAction] Optional function to execute before capturing this frame. +/// * [verifyAction] Optional function to verify the state after capturing this frame. +/// +/// {@category Configuration} +class GoldenAnimationStep { + const GoldenAnimationStep({ + required this.timestamp, + required this.frameName, + this.setupAction, + this.verifyAction, + }); + + final Duration timestamp; + final String frameName; + final Future Function(WidgetTester)? setupAction; + final Future Function(WidgetTester)? verifyAction; +} + +/// ## GoldenAnimationConfig +/// Configuration class for capturing golden tests of animations. +/// +/// This class extends `GoldenCaptureConfig` to provide animation-specific +/// configuration options including timeline settings and frame capture details. +/// +/// * [testName] The name of the test, used to identify the golden file. +/// * [totalDuration] The total duration of the animation to be tested. +/// * [animationSteps] List of specific frames to capture during the animation. +/// * [showTimelineLabels] Whether to display timestamp labels on the combined image. +/// * [timelineColor] Color for the timeline visualization in the golden image. +/// * [layoutType] How to arrange the captured frames (vertical, horizontal, grid). +/// * [maxScreensPerRow] Maximum frames per row when using grid layout. +/// * [spacing] Space between frames in the combined image. +/// * [device] Optional device configuration for consistent rendering. +/// +/// {@category Configuration} +class GoldenAnimationConfig extends GoldenCaptureConfig { + const GoldenAnimationConfig({ + required super.testName, + required this.totalDuration, + required this.animationSteps, + this.showTimelineLabels = true, + this.timelineColor = Colors.blue, + super.layoutType = CaptureLayoutType.horizontal, + super.maxScreensPerRow = 5, + super.spacing = 24.0, + super.device, + }); + + final Duration totalDuration; + final List animationSteps; + final bool showTimelineLabels; + final Color timelineColor; + + /// Validates that all animation steps are within the total duration. + bool get isValid { + return animationSteps.every( + (step) => step.timestamp <= totalDuration, + ); + } + + /// Gets the animation steps sorted by timestamp. + List get sortedSteps { + final steps = List.from(animationSteps); + steps.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + + return steps; + } +} diff --git a/lib/src/helpers/logger.dart b/lib/src/helpers/logger.dart index 7b9e3ff..d256e43 100644 --- a/lib/src/helpers/logger.dart +++ b/lib/src/helpers/logger.dart @@ -23,14 +23,14 @@ void log( Object? error, StackTrace? stackTrace, }) { - logger.log(level, message, error: error, stackTrace: stackTrace); + logger.log(level, message, error, stackTrace); } void logDebug(String message) => log(Level.debug, message); void logInfo(String message) => log(Level.info, message); void logWarning(String message) => log(Level.warning, message); void logError(String message) => log(Level.error, message); -void logVerbose(String message) => log(Level.trace, message); +void logVerbose(String message) => log(Level.verbose, message); void logException(Object error, [StackTrace? stackTrace]) { log(Level.error, 'Exception', error: error, stackTrace: stackTrace); diff --git a/lib/src/testkit/golden_testing_tools.dart b/lib/src/testkit/golden_testing_tools.dart index 863940a..f7336d0 100644 --- a/lib/src/testkit/golden_testing_tools.dart +++ b/lib/src/testkit/golden_testing_tools.dart @@ -5,6 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; +import '../capture/golden_animation_capture.dart'; +import '../config/golden_animation_config.dart'; import '../config/golden_capture_config.dart'; import '../helpers/helpers.dart'; import '../helpers/logger.dart'; @@ -30,7 +32,7 @@ class BcGoldenCapture { String description, Future Function(WidgetTester) test, { bool shouldUseRealShadows = true, - Level logLevel = Level.off, + Level logLevel = Level.nothing, }) { setLogLevel(logLevel); @@ -86,97 +88,177 @@ class BcGoldenCapture { String description, List steps, GoldenCaptureConfig config, { - Level logLevel = Level.off, + Level logLevel = Level.nothing, bool shouldUseRealShadows = true, }) { - final GoldenScreenshot screenshotter = GoldenScreenshot(); setLogLevel(logLevel); - testWidgets(description, (tester) async { - if (config.device != null) { - tester.configureWindow(config.device!); - } + testWidgets( + description, + (tester) async { + if (config.device != null) { + tester.configureWindow(config.device!); + } - await loadAppFonts(); + await loadAppFonts(); - await tester.awaitImages(); + await tester.awaitImages(); - final initialDebugDisableShadowsValue = debugDisableShadows; - debugDisableShadows = !shouldUseRealShadows; + final initialDebugDisableShadowsValue = debugDisableShadows; + debugDisableShadows = !shouldUseRealShadows; - try { - for (int index = 0; index < steps.length; index++) { - final step = steps[index]; + try { + for (int index = 0; index < steps.length; index++) { + final step = steps[index]; - logDebug( - '[flows][multiple] Rendering step ${index + 1}/${steps.length}: ${step.stepName}', - ); + logDebug( + '[flows][multiple] Rendering step ${index + 1}/${steps.length}: ${step.stepName}', + ); - await tester.pumpWidget( - TestBase.appGoldenTest( - widget: step.widgetBuilder(), - key: GlobalKey(), - ), - ); + await tester.pumpWidget( + TestBase.appGoldenTest( + widget: step.widgetBuilder(), + key: GlobalKey(), + ), + ); - logDebug( - '[flows][multiple] ✓ Rendered step ${index + 1}/${steps.length}: ${step.stepName}', - ); + logDebug( + '[flows][multiple] ✓ Rendered step ${index + 1}/${steps.length}: ${step.stepName}', + ); + + if (step.setupAction != null) { + await step.setupAction!(tester); + } + + logDebug( + '[flows][multiple] ✓ Setup action completed for step ${index + 1}/${steps.length}: ${step.stepName}', + ); + + await tester.pumpAndSettle(); - if (step.setupAction != null) { - await step.setupAction!(tester); + if (index > 0) { + await tester.pump(config.delayBetweenScreens); + } + + if (step.verifyAction != null) { + await step.verifyAction!(tester); + } + + logDebug( + '[flows][multiple] ✓ Verify action completed for step ${index + 1}/${steps.length}: ${step.stepName}', + ); + + await tester.runAsync(() async { + await tester.captureGoldenScreenshot(); + logDebug( + '[flows][multiple] ✓ Captured screenshot for step ${index + 1}/${steps.length}: ${step.stepName}', + ); + }); } - logDebug( - '[flows][multiple] ✓ Setup action completed for step ${index + 1}/${steps.length}: ${step.stepName}', + logDebug('[flows][multiple] Combining screenshots...'); + } finally { + debugDisableShadows = initialDebugDisableShadowsValue; + } + + await tester.runAsync(() async { + final combinedImage = await tester.combineGoldenScreenshots( + config, + steps.map((s) => s.stepName).toList(), ); - await tester.pumpAndSettle(); + logDebug('[flows][multiple] ✓ Combined screenshots successfully.'); - if (index > 0) { - await tester.pump(config.delayBetweenScreens); - } + final testPath = + (goldenFileComparator as LocalFileComparator).basedir.path; - if (step.verifyAction != null) { - await step.verifyAction!(tester); - } + await localFileComparator(testPath); - logDebug( - '[flows][multiple] ✓ Verify action completed for step ${index + 1}/${steps.length}: ${step.stepName}', + await expectLater( + combinedImage, + matchesGoldenFile('goldens/${config.testName}_flow.png'), ); + }); + }, + tags: ['golden'], + ); + } - await tester.runAsync(() async { - await screenshotter.captureScreenshot(); - logDebug( - '[flows][multiple] ✓ Captured screenshot for step ${index + 1}/${steps.length}: ${step.stepName}', + /// ## animation + /// Function to capture golden tests of animations at specific timestamps. + /// This function is tagged with 'golden'. + /// + /// Creates a comprehensive visual test that shows an animation's progression + /// by capturing frames at specified timestamps and combining them into a single image. + /// + /// - [description] A brief description of the animation test. + /// - [widget] The animated widget to test. + /// - [steps] List of animation steps defining when to capture frames. + /// - [config] Configuration for the animation capture including layout and timing. + /// - [animationSetup] Optional function to set up the animation before starting. + /// - [shouldUseRealShadows] Whether to render shadows or not. + /// - [logLevel] The log level for the test, defaulting to `Level.off`. + @isTest + static void animation( + String description, + Widget widget, + GoldenAnimationConfig config, { + Future Function(WidgetTester)? animationSetup, + bool shouldUseRealShadows = true, + Level logLevel = Level.nothing, + }) { + setLogLevel(logLevel); + + testWidgets( + description, + (widgetTester) async { + //ignore: always_declare_return_types + body() async { + logDebug('[golden][animation] Starting animation test: $description'); + + if (!config.isValid) { + throw ArgumentError( + 'Animation configuration is invalid. All steps must be within totalDuration.', ); - }); - } + } - logDebug('[flows][multiple] Combining screenshots...'); - } finally { - debugDisableShadows = initialDebugDisableShadowsValue; - } + final initialDebugDisableShadowsValue = debugDisableShadows; + debugDisableShadows = !shouldUseRealShadows; - await tester.runAsync(() async { - final combinedImage = await screenshotter.combineScreenshots( - config, - steps.map((s) => s.stepName).toList(), - ); + try { + logDebug('[golden][animation] Setting up animation capture...'); + + final combinedImage = await widgetTester.captureAnimation( + TestBase.appGoldenTest( + widget: widget, + key: GlobalKey(), + ), + config, + animationSetup, + ); - logDebug('[flows][multiple] ✓ Combined screenshots successfully.'); + logDebug('[golden][animation] Comparing with golden file...'); + final testPath = + (goldenFileComparator as LocalFileComparator).basedir.path; + await localFileComparator(testPath); - final testPath = - (goldenFileComparator as LocalFileComparator).basedir.path; + await expectLater( + combinedImage, + matchesGoldenFile('goldens/${config.testName}_animation.png'), + ); - await localFileComparator(testPath); + logDebug( + '[golden][animation] ✓ Animation test completed: $description', + ); + } finally { + debugDisableShadows = initialDebugDisableShadowsValue; + } + } - await expectLater( - combinedImage, - matchesGoldenFile('goldens/${config.testName}_flow.png'), - ); - }); - }); + await widgetTester.runAsync(body); + }, + tags: ['golden'], + ); } } @@ -195,7 +277,7 @@ void bcGoldenTest( String description, Future Function(WidgetTester) test, { bool shouldUseRealShadows = true, - Level logLevel = Level.off, + Level logLevel = Level.nothing, }) { BcGoldenCapture.single( description, diff --git a/pubspec.yaml b/pubspec.yaml index 9cdf016..2232993 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: bc_golden_plugin description: Golden Plugin is a software that enables you to test the UI you develop against a reference image or a design in Figma. repository: 'https://github.com/bancolombia/bc_golden_plugin' -version: 2.0.0-alpha.2 +version: 2.0.0-alpha.3 platforms: android: @@ -12,8 +12,7 @@ platforms: linux: environment: - sdk: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" + sdk: ">=3.0.0 <4.0.0" dependencies: @@ -21,13 +20,13 @@ dependencies: sdk: flutter flutter: sdk: flutter - platform: ^3.1.6 - file: ^7.0.0 + platform: ^3.1.0 + file: ^6.1.4 provider: ^6.1.2 equatable: ^2.0.5 meta: ^1.9.1 flutter_svg: ^2.2.0 - logger: ^2.6.1 + logger: ^1.0.0 dev_dependencies: flutter_lints: ^3.0.0 diff --git a/test/src/capture/golden_animation_capture_test.dart b/test/src/capture/golden_animation_capture_test.dart new file mode 100644 index 0000000..f2cef52 --- /dev/null +++ b/test/src/capture/golden_animation_capture_test.dart @@ -0,0 +1,549 @@ +import 'dart:typed_data'; + +import 'package:bc_golden_plugin/bc_golden_plugin.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('GoldenAnimationCapture extension', () { + late Widget testWidget; + + setUp(() { + testWidget = const MaterialApp( + home: Scaffold( + body: Center( + child: ColoredBox( + color: Colors.blue, + child: SizedBox( + width: 100, + height: 100, + ), + ), + ), + ), + ); + }); + + group('captureAnimation', () { + testWidgets( + 'throws ArgumentError when config is invalid', + (tester) async { + const invalidConfig = GoldenAnimationConfig( + testName: 'invalid_test', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 200), // Beyond totalDuration + frameName: 'invalid_step', + ), + ], + ); + + expect( + () => tester.captureAnimation(testWidget, invalidConfig, null), + throwsA(isA()), + ); + }, + ); + + testWidgets('captures animation with valid config', (tester) async { + await tester.runAsync(() async { + const config = GoldenAnimationConfig( + testName: 'simple_animation', + totalDuration: Duration(milliseconds: 300), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 0), + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 150), + frameName: 'middle', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 300), + frameName: 'end', + ), + ], + ); + + final result = + await tester.captureAnimation(testWidget, config, null); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }); + + testWidgets('executes animation setup function', (tester) async { + await tester.runAsync(() async { + bool setupCalled = false; + + const config = GoldenAnimationConfig( + testName: 'setup_test', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 0), + frameName: 'frame', + ), + ], + ); + + await tester.captureAnimation( + testWidget, + config, + (tester) async { + setupCalled = true; + }, + ); + + expect(setupCalled, isTrue); + }); + }); + + testWidgets('executes step setup and verify actions', (tester) async { + await tester.runAsync(() async { + bool setupActionCalled = false; + bool verifyActionCalled = false; + + final config = GoldenAnimationConfig( + testName: 'actions_test', + totalDuration: const Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: const Duration(milliseconds: 0), + frameName: 'frame', + setupAction: (tester) async { + setupActionCalled = true; + }, + verifyAction: (tester) async { + verifyActionCalled = true; + }, + ), + ], + ); + + await tester.captureAnimation(testWidget, config, null); + + expect(setupActionCalled, isTrue); + expect(verifyActionCalled, isTrue); + }); + }); + + testWidgets('sorts animation steps by timestamp', (tester) async { + await tester.runAsync(() async { + const config = GoldenAnimationConfig( + testName: 'sorting_test', + totalDuration: Duration(milliseconds: 300), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 200), + frameName: 'second', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 0), + frameName: 'first', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 100), + frameName: 'middle', + ), + ], + ); + + // This should not throw and should handle the sorting internally + final result = + await tester.captureAnimation(testWidget, config, null); + expect(result, isA()); + }); + }); + + testWidgets('handles single animation step', (tester) async { + await tester.runAsync(() async { + const config = GoldenAnimationConfig( + testName: 'single_step', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 50), + frameName: 'only_frame', + ), + ], + ); + + final result = + await tester.captureAnimation(testWidget, config, null); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }); + + testWidgets('handles zero duration steps', (tester) async { + await tester.runAsync(() async { + const config = GoldenAnimationConfig( + testName: 'zero_duration', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration.zero, // Same timestamp + frameName: 'also_start', + ), + ], + ); + + final result = + await tester.captureAnimation(testWidget, config, null); + expect(result, isA()); + }); + }); + }); + + group( + 'combineAnimationScreenshots', + () { + testWidgets( + 'throws ArgumentError when screenshots are empty', + (tester) async { + const config = GoldenAnimationConfig( + testName: 'empty_test', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'frame', + ), + ], + ); + + // Clear any existing screenshots + tester.clearGoldenScreenshots(); + + expect( + () => tester.combineAnimationScreenshots(config, ['frame']), + throwsA(isA()), + ); + }, + ); + + testWidgets('combines screenshots successfully', (tester) async { + await tester.runAsync(() async { + // First capture some screenshots + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + // Capture a few screenshots to test combination + await tester.captureGoldenScreenshot(); + await tester.captureGoldenScreenshot(); + + const config = GoldenAnimationConfig( + testName: 'combine_test', + totalDuration: Duration(milliseconds: 200), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'frame1', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 100), + frameName: 'frame2', + ), + ], + ); + + final result = await tester.combineAnimationScreenshots( + config, + ['frame1 - 0:00:00.000000', 'frame2 - 0:00:00.100000'], + ); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }); + + testWidgets( + 'handles large canvas dimensions with scaling', + (tester) async { + await tester.runAsync(() async { + // Create a large widget that would exceed canvas limits + final largeWidget = MaterialApp( + home: Scaffold( + body: Center( + child: Container( + width: 2000, + height: 2000, + color: Colors.red, + ), + ), + ), + ); + + await tester.pumpWidget(largeWidget); + await tester.pumpAndSettle(); + + // Capture multiple screenshots to force large canvas + for (int i = 0; i < 3; i++) { + await tester.captureGoldenScreenshot(); + } + + const config = GoldenAnimationConfig( + testName: 'large_canvas_test', + totalDuration: Duration(milliseconds: 300), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'frame1', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 100), + frameName: 'frame2', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 200), + frameName: 'frame3', + ), + ], + ); + + final result = await tester.combineAnimationScreenshots( + config, + [ + 'frame1 - 0:00:00.000000', + 'frame2 - 0:00:00.100000', + 'frame3 - 0:00:00.200000', + ], + ); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }, + ); + + testWidgets( + 'handles many screenshots with grid layout', + (tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(testWidget); + await tester.pumpAndSettle(); + + // Capture more than 5 screenshots to trigger grid layout + for (int i = 0; i < 7; i++) { + await tester.captureGoldenScreenshot(); + } + + final config = GoldenAnimationConfig( + testName: 'grid_layout_test', + totalDuration: const Duration(milliseconds: 700), + animationSteps: List.generate( + 7, + (index) => GoldenAnimationStep( + timestamp: Duration(milliseconds: index * 100), + frameName: 'frame${index + 1}', + ), + ), + maxScreensPerRow: 3, + ); + + final stepNames = List.generate( + 7, + (index) => + 'frame${index + 1} - 0:00:00.${(index * 100).toString().padLeft(6, '0')}', + ); + + final result = + await tester.combineAnimationScreenshots(config, stepNames); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }, + ); + }, + ); + + group('integration tests', () { + testWidgets('complete animation capture workflow', (tester) async { + await tester.runAsync(() async { + final animatedWidget = MaterialApp( + home: Scaffold( + body: TweenAnimationBuilder( + duration: const Duration(milliseconds: 500), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Center( + child: Transform.scale( + scale: value, + child: Container( + width: 100, + height: 100, + color: Colors.blue, + ), + ), + ); + }, + ), + ), + ); + + const config = GoldenAnimationConfig( + testName: 'integration_test', + totalDuration: Duration(milliseconds: 500), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 0), + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 250), + frameName: 'middle', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 500), + frameName: 'end', + ), + ], + spacing: 20.0, + maxScreensPerRow: 3, + ); + + final result = await tester.captureAnimation( + animatedWidget, + config, + (tester) async { + // Setup animation by triggering rebuild with animation + await tester.pump(); + }, + ); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + + // Verify that screenshots were captured + expect(tester.goldenScreenshots.length, equals(3)); + }); + }); + + testWidgets( + 'handles custom spacing and layout configuration', + (tester) async { + await tester.runAsync(() async { + const config = GoldenAnimationConfig( + testName: 'custom_spacing_test', + totalDuration: Duration(milliseconds: 200), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 0), + frameName: 'frame1', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 100), + frameName: 'frame2', + ), + ], + spacing: 50.0, + maxScreensPerRow: 2, + ); + + final result = + await tester.captureAnimation(testWidget, config, null); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }, + ); + }); + + group('edge cases', () { + testWidgets( + 'handles widget that changes size during animation', + (tester) async { + await tester.runAsync(() async { + bool isLarge = false; + + final changingSizeWidget = StatefulBuilder( + builder: (context, setState) { + return MaterialApp( + home: Scaffold( + body: Center( + child: GestureDetector( + onTap: () { + setState(() { + isLarge = !isLarge; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + width: isLarge ? 200 : 100, + height: isLarge ? 200 : 100, + color: Colors.green, + ), + ), + ), + ), + ); + }, + ); + + final config = GoldenAnimationConfig( + testName: 'changing_size_test', + totalDuration: const Duration(milliseconds: 300), + animationSteps: [ + GoldenAnimationStep( + timestamp: const Duration(milliseconds: 0), + frameName: 'small', + setupAction: (tester) async { + // Trigger size change + await tester.tap(find.byType(GestureDetector)); + await tester.pump(); + }, + ), + const GoldenAnimationStep( + timestamp: Duration(milliseconds: 150), + frameName: 'transitioning', + ), + const GoldenAnimationStep( + timestamp: Duration(milliseconds: 300), + frameName: 'large', + ), + ], + ); + + final result = + await tester.captureAnimation(changingSizeWidget, config, null); + + expect(result, isA()); + expect(result.isNotEmpty, isTrue); + }); + }, + ); + + testWidgets('handles empty animation steps list', (tester) async { + const config = GoldenAnimationConfig( + testName: 'empty_steps', + totalDuration: Duration(milliseconds: 100), + animationSteps: [], // Empty list + ); + + // Empty list is valid but will throw when trying to combine + expect(config.isValid, isTrue); // Empty list is valid + + await tester.runAsync(() async { + // Should throw ArgumentError when trying to combine empty screenshots + expect( + () => tester.captureAnimation(testWidget, config, null), + throwsA(isA()), + ); + }); + }); + }); + }); +} diff --git a/test/src/capture/golden_screenshot_test.dart b/test/src/capture/golden_screenshot_test.dart index 93367a5..686c314 100644 --- a/test/src/capture/golden_screenshot_test.dart +++ b/test/src/capture/golden_screenshot_test.dart @@ -14,79 +14,45 @@ void main() { return frame.image; } - group('GoldenScreenshot.add', () { - test('add valid screenshots and rejects empty ones', () { - final goldenScreenshot = GoldenScreenshot(); - - expect(goldenScreenshot.screenshots, isEmpty); - - expect(goldenScreenshot.screenshots, isA>()); - }); - }); + Widget createColorWidget(Color color, Size size) { + return Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: RepaintBoundary( + child: SizedBox( + width: size.width, + height: size.height, + child: ColoredBox(color: color), + ), + ), + ), + ); + } - group('GoldenScreenshot.captureScreenshot', () { + group('GoldenScreenshot extension', () { testWidgets( 'captures a PNG image from RepaintBoundary widget.', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - const size = Size(120, 80); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Align( - alignment: Alignment.topLeft, - child: RepaintBoundary( - child: SizedBox( - width: size.width, - height: size.height, - child: const ColoredBox(color: Colors.blue), - ), - ), - ), - ), - ); - + await tester.pumpWidget(createColorWidget(Colors.blue, size)); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); - - expect(goldenScreenshot.screenshots, hasLength(1)); + final screenshot = await tester.captureGoldenScreenshot(); - final bytes = goldenScreenshot.screenshots.first; - expect(bytes, isNotEmpty); + expect(screenshot, isNotEmpty); - final img = await decode(bytes); + final img = await decode(screenshot); expect(img.width, equals(size.width.toInt())); expect(img.height, equals(size.height.toInt())); img.dispose(); }); }, ); - }); - - group('GoldenScreenshot.combineScreenshots', () { - Widget createColorWidget(Color color, Size size) { - return Directionality( - textDirection: TextDirection.ltr, - child: Align( - alignment: Alignment.topLeft, - child: RepaintBoundary( - child: SizedBox( - width: size.width, - height: size.height, - child: ColoredBox(color: color), - ), - ), - ), - ); - } testWidgets('throws ArgumentError if the list is empty.', (tester) async { - final goldenScreenshot = GoldenScreenshot(); - const config = GoldenCaptureConfig( testName: 'Test', layoutType: CaptureLayoutType.vertical, @@ -95,24 +61,22 @@ void main() { ); expect( - () => goldenScreenshot.combineScreenshots(config, []), + () => tester.combineGoldenScreenshots(config, []), throwsA(isA()), ); }); testWidgets('combines in vertical layout', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - await tester .pumpWidget(createColorWidget(Colors.red, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester .pumpWidget(createColorWidget(Colors.green, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); const config = GoldenCaptureConfig( testName: 'Test', @@ -121,7 +85,7 @@ void main() { maxScreensPerRow: 2, ); - final combined = await goldenScreenshot.combineScreenshots( + final combined = await tester.combineGoldenScreenshots( config, ['Step A', 'Step B'], ); @@ -137,22 +101,20 @@ void main() { testWidgets('combines in horizontal layout', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - await tester .pumpWidget(createColorWidget(Colors.red, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester .pumpWidget(createColorWidget(Colors.green, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester .pumpWidget(createColorWidget(Colors.blue, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); const config = GoldenCaptureConfig( testName: 'Test', @@ -161,7 +123,7 @@ void main() { maxScreensPerRow: 3, ); - final combined = await goldenScreenshot.combineScreenshots( + final combined = await tester.combineGoldenScreenshots( config, ['A', 'B', 'C'], ); @@ -178,26 +140,20 @@ void main() { 'combines in grid layout with maxScreensPerRow', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - await tester .pumpWidget(createColorWidget(Colors.red, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester.pumpWidget( - createColorWidget( - Colors.green, - const Size(100, 180), - ), - ); + createColorWidget(Colors.green, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester .pumpWidget(createColorWidget(Colors.blue, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); const config = GoldenCaptureConfig( testName: 'Test', @@ -206,7 +162,7 @@ void main() { maxScreensPerRow: 2, ); - final combined = await goldenScreenshot.combineScreenshots( + final combined = await tester.combineGoldenScreenshots( config, ['1', '2', '3'], ); @@ -222,8 +178,6 @@ void main() { testWidgets('respects canvas limit and expands if needed', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - for (int i = 0; i < 5; i++) { await tester.pumpWidget( createColorWidget( @@ -232,7 +186,7 @@ void main() { ), ); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); } const config = GoldenCaptureConfig( @@ -242,12 +196,11 @@ void main() { maxScreensPerRow: 2, ); final names = List.generate( - goldenScreenshot.screenshots.length, + tester.goldenScreenshots.length, (i) => 'Step ${i + 1}', ); - final combined = - await goldenScreenshot.combineScreenshots(config, names); + final combined = await tester.combineGoldenScreenshots(config, names); final img = await decode(combined); expect(img.width, lessThanOrEqualTo(4096)); @@ -260,21 +213,15 @@ void main() { 'fails if stepnames list is shorter than screenshots', (tester) async { await tester.runAsync(() async { - final goldenScreenshot = GoldenScreenshot(); - await tester .pumpWidget(createColorWidget(Colors.red, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); await tester.pumpWidget( - createColorWidget( - Colors.green, - const Size(100, 180), - ), - ); + createColorWidget(Colors.green, const Size(100, 180))); await tester.pumpAndSettle(); - await goldenScreenshot.captureScreenshot(); + await tester.captureGoldenScreenshot(); const config = GoldenCaptureConfig( testName: 'Test', @@ -284,7 +231,7 @@ void main() { ); expect( - () => goldenScreenshot.combineScreenshots( + () => tester.combineGoldenScreenshots( config, ['Only one'], ), diff --git a/test/src/config/golden_animation_config_test.dart b/test/src/config/golden_animation_config_test.dart new file mode 100644 index 0000000..c1c61c5 --- /dev/null +++ b/test/src/config/golden_animation_config_test.dart @@ -0,0 +1,143 @@ +import 'package:bc_golden_plugin/bc_golden_plugin.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('GoldenAnimationConfig', () { + test('should validate animation steps within total duration', () { + const config = GoldenAnimationConfig( + testName: 'test_animation', + totalDuration: Duration(milliseconds: 500), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 250), + frameName: 'middle', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 500), + frameName: 'end', + ), + ], + ); + + expect(config.isValid, isTrue); + }); + + test('should invalidate animation steps beyond total duration', () { + const config = GoldenAnimationConfig( + testName: 'test_animation', + totalDuration: Duration(milliseconds: 500), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'start', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 600), // Beyond totalDuration + frameName: 'invalid', + ), + ], + ); + + expect(config.isValid, isFalse); + }); + + test('should sort animation steps by timestamp', () { + const config = GoldenAnimationConfig( + testName: 'test_animation', + totalDuration: Duration(milliseconds: 500), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 300), + frameName: 'third', + ), + GoldenAnimationStep( + timestamp: Duration.zero, + frameName: 'first', + ), + GoldenAnimationStep( + timestamp: Duration(milliseconds: 150), + frameName: 'second', + ), + ], + ); + + final sortedSteps = config.sortedSteps; + + expect(sortedSteps[0].frameName, equals('first')); + expect(sortedSteps[1].frameName, equals('second')); + expect(sortedSteps[2].frameName, equals('third')); + }); + + test('should have correct default values', () { + const config = GoldenAnimationConfig( + testName: 'test_animation', + totalDuration: Duration(milliseconds: 500), + animationSteps: [], + ); + + expect(config.showTimelineLabels, isTrue); + expect(config.timelineColor, equals(Colors.blue)); + expect(config.layoutType, equals(CaptureLayoutType.horizontal)); + expect(config.maxScreensPerRow, equals(5)); + expect(config.spacing, equals(24.0)); + }); + }); + + group('GoldenAnimationStep', () { + test('should create step with required parameters', () { + const step = GoldenAnimationStep( + timestamp: Duration(milliseconds: 100), + frameName: 'test_frame', + ); + + expect(step.timestamp, equals(const Duration(milliseconds: 100))); + expect(step.frameName, equals('test_frame')); + expect(step.setupAction, isNull); + expect(step.verifyAction, isNull); + }); + + test('should create step with optional actions', () { + Future mockSetupAction(WidgetTester tester) async {} + Future mockVerifyAction(WidgetTester tester) async {} + + final step = GoldenAnimationStep( + timestamp: const Duration(milliseconds: 100), + frameName: 'test_frame', + setupAction: mockSetupAction, + verifyAction: mockVerifyAction, + ); + + expect(step.setupAction, isNotNull); + expect(step.verifyAction, isNotNull); + }); + }); + + group('GoldenAnimationCapture', () { + testWidgets('should throw error for invalid configuration', (tester) async { + const invalidConfig = GoldenAnimationConfig( + testName: 'test_animation', + totalDuration: Duration(milliseconds: 100), + animationSteps: [ + GoldenAnimationStep( + timestamp: Duration(milliseconds: 200), // Beyond totalDuration + frameName: 'invalid', + ), + ], + ); + + expect( + () async => await tester.captureAnimation( + const SizedBox(), + invalidConfig, + (WidgetTester tester) async {}, + ), + throwsArgumentError, + ); + }); + }); +} diff --git a/test/src/helpers/logger_test.dart b/test/src/helpers/logger_test.dart index 494e8e2..dfc508f 100644 --- a/test/src/helpers/logger_test.dart +++ b/test/src/helpers/logger_test.dart @@ -36,7 +36,7 @@ void main() { log(Level.info, message, error: error, stackTrace: stackTrace); verify( - mockLogger.log(Level.info, message, error: error, stackTrace: stackTrace), + mockLogger.log(Level.info, message, error, stackTrace), ).called(1); }); @@ -67,6 +67,6 @@ void main() { test('logVerbose should call log with Level.trace', () { const message = 'Verbose message'; logVerbose(message); - verify(mockLogger.log(Level.trace, message)).called(1); + verify(mockLogger.log(Level.verbose, message)).called(1); }); } diff --git a/test/src/helpers/logger_test.mocks.dart b/test/src/helpers/logger_test.mocks.dart index fe076c8..3a29ac2 100644 --- a/test/src/helpers/logger_test.mocks.dart +++ b/test/src/helpers/logger_test.mocks.dart @@ -3,9 +3,6 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; - -import 'package:logger/src/log_level.dart' as _i4; import 'package:logger/src/logger.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; @@ -27,193 +24,130 @@ import 'package:mockito/mockito.dart' as _i1; /// /// See the documentation for Mockito's code generation for more information. class MockLogger extends _i1.Mock implements _i2.Logger { - @override - _i3.Future get init => (super.noSuchMethod( - Invocation.getter(#init), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); - @override void v( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #v, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, - ), - returnValueForMissingStub: null, - ); - - @override - void t( - dynamic message, { - DateTime? time, - Object? error, - StackTrace? stackTrace, - }) => - super.noSuchMethod( - Invocation.method( - #t, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void d( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #d, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void i( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #i, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void w( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #w, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void e( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #e, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void wtf( - dynamic message, { - DateTime? time, - Object? error, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #wtf, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, - ), - returnValueForMissingStub: null, - ); - - @override - void f( - dynamic message, { - DateTime? time, - Object? error, - StackTrace? stackTrace, - }) => - super.noSuchMethod( - Invocation.method( - #f, - [message], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, + [ + message, + error, + stackTrace, + ], ), returnValueForMissingStub: null, ); @override void log( - _i4.Level? level, - dynamic message, { - DateTime? time, - Object? error, + _i2.Level? level, + dynamic message, [ + dynamic error, StackTrace? stackTrace, - }) => + ]) => super.noSuchMethod( Invocation.method( #log, [ level, message, + error, + stackTrace, ], - { - #time: time, - #error: error, - #stackTrace: stackTrace, - }, ), returnValueForMissingStub: null, ); @@ -229,12 +163,11 @@ class MockLogger extends _i1.Mock implements _i2.Logger { ) as bool); @override - _i3.Future close() => (super.noSuchMethod( + void close() => super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValueForMissingStub: null, + ); } diff --git a/test/src/testkit/golden_testing_tools_test.dart b/test/src/testkit/golden_testing_tools_test.dart index b0f3b60..70bf34b 100644 --- a/test/src/testkit/golden_testing_tools_test.dart +++ b/test/src/testkit/golden_testing_tools_test.dart @@ -1,12 +1,10 @@ import 'package:bc_golden_plugin/bc_golden_plugin.dart'; -import 'package:bc_golden_plugin/src/capture/golden_screenshot.dart'; import 'package:bc_golden_plugin/src/testkit/window_size.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @GenerateNiceMocks([ - MockSpec(), MockSpec(), ]) class FakeGoldenStep implements GoldenStep { diff --git a/test/src/testkit/golden_testing_tools_test.mocks.dart b/test/src/testkit/golden_testing_tools_test.mocks.dart index 45abb12..b3b1c6d 100644 --- a/test/src/testkit/golden_testing_tools_test.mocks.dart +++ b/test/src/testkit/golden_testing_tools_test.mocks.dart @@ -3,15 +3,10 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:typed_data' as _i5; - -import 'package:bc_golden_plugin/src/capture/golden_screenshot.dart' as _i4; -import 'package:bc_golden_plugin/src/config/golden_capture_config.dart' as _i7; -import 'package:flutter/material.dart' as _i2; -import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/src/widgets/framework.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i8; +import 'package:mockito/src/dummies.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -37,12 +32,12 @@ class _FakeElement_0 extends _i1.SmartFake implements _i2.Element { ); @override - String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); } class _FakeDiagnosticsNode_1 extends _i1.SmartFake - implements _i2.DiagnosticsNode { + implements _i3.DiagnosticsNode { _FakeDiagnosticsNode_1( Object parent, Invocation parentInvocation, @@ -54,70 +49,11 @@ class _FakeDiagnosticsNode_1 extends _i1.SmartFake @override String toString({ _i3.TextTreeConfiguration? parentConfiguration, - _i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, }) => super.toString(); } -/// A class which mocks [GoldenScreenshot]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockGoldenScreenshot extends _i1.Mock implements _i4.GoldenScreenshot { - @override - List<_i5.Uint8List> get screenshots => (super.noSuchMethod( - Invocation.getter(#screenshots), - returnValue: <_i5.Uint8List>[], - returnValueForMissingStub: <_i5.Uint8List>[], - ) as List<_i5.Uint8List>); - - @override - void add(_i5.Uint8List? screenshot) => super.noSuchMethod( - Invocation.method( - #add, - [screenshot], - ), - returnValueForMissingStub: null, - ); - - @override - void addAll(Iterable<_i5.Uint8List>? screenshots) => super.noSuchMethod( - Invocation.method( - #addAll, - [screenshots], - ), - returnValueForMissingStub: null, - ); - - @override - _i6.Future<_i5.Uint8List> captureScreenshot() => (super.noSuchMethod( - Invocation.method( - #captureScreenshot, - [], - ), - returnValue: _i6.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - returnValueForMissingStub: - _i6.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - ) as _i6.Future<_i5.Uint8List>); - - @override - _i6.Future<_i5.Uint8List> combineScreenshots( - _i7.GoldenFlowConfig? config, - List? stepNames, - ) => - (super.noSuchMethod( - Invocation.method( - #combineScreenshots, - [ - config, - stepNames, - ], - ), - returnValue: _i6.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - returnValueForMissingStub: - _i6.Future<_i5.Uint8List>.value(_i5.Uint8List(0)), - ) as _i6.Future<_i5.Uint8List>); -} - /// A class which mocks [Widget]. /// /// See the documentation for Mockito's code generation for more information. @@ -151,14 +87,14 @@ class MockWidget extends _i1.Mock implements _i2.Widget { #toStringShort, [], ), - returnValue: _i8.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #toStringShort, [], ), ), - returnValueForMissingStub: _i8.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.method( #toStringShort, @@ -180,7 +116,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { @override String toStringShallow({ String? joiner = ', ', - _i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.debug, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, }) => (super.noSuchMethod( Invocation.method( @@ -191,7 +127,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { #minLevel: minLevel, }, ), - returnValue: _i8.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #toStringShallow, @@ -202,7 +138,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { }, ), ), - returnValueForMissingStub: _i8.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.method( #toStringShallow, @@ -219,7 +155,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { String toStringDeep({ String? prefixLineOne = '', String? prefixOtherLines, - _i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.debug, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, int? wrapWidth = 65, }) => (super.noSuchMethod( @@ -233,7 +169,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { #wrapWidth: wrapWidth, }, ), - returnValue: _i8.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #toStringDeep, @@ -246,7 +182,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { }, ), ), - returnValueForMissingStub: _i8.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.method( #toStringDeep, @@ -262,7 +198,7 @@ class MockWidget extends _i1.Mock implements _i2.Widget { ) as String); @override - _i2.DiagnosticsNode toDiagnosticsNode({ + _i3.DiagnosticsNode toDiagnosticsNode({ String? name, _i3.DiagnosticsTreeStyle? style, }) => @@ -297,19 +233,19 @@ class MockWidget extends _i1.Mock implements _i2.Widget { }, ), ), - ) as _i2.DiagnosticsNode); + ) as _i3.DiagnosticsNode); @override - List<_i2.DiagnosticsNode> debugDescribeChildren() => (super.noSuchMethod( + List<_i3.DiagnosticsNode> debugDescribeChildren() => (super.noSuchMethod( Invocation.method( #debugDescribeChildren, [], ), - returnValue: <_i2.DiagnosticsNode>[], - returnValueForMissingStub: <_i2.DiagnosticsNode>[], - ) as List<_i2.DiagnosticsNode>); + returnValue: <_i3.DiagnosticsNode>[], + returnValueForMissingStub: <_i3.DiagnosticsNode>[], + ) as List<_i3.DiagnosticsNode>); @override - String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => super.toString(); }