Skip to content

Instantly share code, notes, and snippets.

@stevenosse
Last active January 20, 2025 21:02
Show Gist options
  • Save stevenosse/b191d56cb4b75ed8012c3d04c1d80448 to your computer and use it in GitHub Desktop.
Save stevenosse/b191d56cb4b75ed8012c3d04c1d80448 to your computer and use it in GitHub Desktop.
testWidgets screenshot
/// Originally published on: https://gist.github.com/stevsct/fc84fee8bcc3271e2295d99d7c7ae49d
///
/// Inspired by https://pub.dev/packages/spot
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
extension TestScreenshotUtil on WidgetTester {
Future<void> takeScreenshot({required String name}) async {
final liveElement = binding.renderViewElement!;
late final Uint8List bytes;
await binding.runAsync(() async {
final image = await _captureImage(liveElement);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
return 'Could not take screenshot';
}
bytes = byteData.buffer.asUint8List();
image.dispose();
});
final directory = Directory('./screenshots');
if (!directory.existsSync()) {
directory.createSync();
}
final file = File('./screenshots/$name.png');
file.writeAsBytesSync(bytes);
}
Future<ui.Image> _captureImage(Element element) async {
assert(element.renderObject != null);
RenderObject renderObject = element.renderObject!;
while (!renderObject.isRepaintBoundary) {
// ignore: unnecessary_cast
renderObject = renderObject.parent! as RenderObject;
}
assert(!renderObject.debugNeedsPaint);
final OffsetLayer layer = renderObject.debugLayer! as OffsetLayer;
final ui.Image image = await layer.toImage(renderObject.paintBounds);
if (element.renderObject is RenderBox) {
final expectedSize = (element.renderObject as RenderBox?)!.size;
if (expectedSize.width != image.width || expectedSize.height != image.height) {
// ignore: avoid_print
print(
'Warning: The screenshot captured of ${element.toStringShort()} is '
'larger (${image.width}, ${image.height}) than '
'${element.toStringShort()} (${expectedSize.width}, ${expectedSize.height}) itself.\n'
'Wrap the ${element.toStringShort()} in a RepaintBoundary to be able to capture only that layer. ',
);
}
}
return image;
}
}
@terryjiang2020
Copy link

Where should I put this code to?

@stevenosse
Copy link
Author

Where should I put this code to?

Hi
You could create an utils folder under your testdirectory. Then in your widget test, you'll be able to call the method like this:

await tester.takeScreenshot(name: 'my_test_name');

The file will be located in $PROJECT_DIR/screenshots

@terryjiang2020
Copy link

Got it, thanks Steven!

@amake
Copy link

amake commented Jan 17, 2025

How can I get this to take a screenshot of a very tall widget? I find that it only captures the portion of the widget that fits inside some default viewport.

@stevenosse
Copy link
Author

Hi @amake

I looked into your issue and came up with this:

Future<void> takeTallScreenshot({required Widget widget, required String name}) async {
    final repaintBoundary = GlobalKey();

    final testWidget = SingleChildScrollView(
      child: RepaintBoundary(
        key: repaintBoundary,
        child: widget,
      ),
    );

    await pumpWidget(MaterialApp(home: testWidget));

    late final Uint8List bytes;
    await binding.runAsync(() async {
      final image = await _captureImageFromRepaintBoundary(repaintBoundary);
      final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      if (byteData == null) {
        return 'Could not take screenshot';
      }
      bytes = byteData.buffer.asUint8List();
      image.dispose();
    });

    final directory = Directory('./screenshots');
    if (!directory.existsSync()) {
      directory.createSync();
    }
    final file = File('./screenshots/$name.png');
    file.writeAsBytesSync(bytes);
  }

  Future<ui.Image> _captureImageFromRepaintBoundary(GlobalKey repaintBoundaryKey) async {
    final renderObject = repaintBoundaryKey.currentContext?.findRenderObject();
    if (renderObject is! RenderRepaintBoundary) {
      throw Exception('No RenderRepaintBoundary found!');
    }
    final context = repaintBoundaryKey.currentContext!;
    return await renderObject.toImage(pixelRatio: View.of(context).devicePixelRatio);
  }

Sample usage:

testWidgets('Capture tall widget screenshot', (WidgetTester tester) async {
    final tallWidget = Column(
      children: List.generate(
        50,
        (index) => Container(
          height: 100,
          color: index.isEven ? Colors.blue : Colors.green,
          child: Center(child: Text('Item $index')),
        ),
      ),
    );

    await tester.takeTallScreenshot(
      widget: tallWidget,
      name: 'tall_widget_screenshot',
    );
  });

This solution is probably not complete but i think it can still be useful for your usecase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment