Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Craftplacer/6be2e7eea3c7ad2f1f36e13c53e50f6b to your computer and use it in GitHub Desktop.
Save Craftplacer/6be2e7eea3c7ad2f1f36e13c53e50f6b to your computer and use it in GitHub Desktop.

Utilizing Flutter's testing framework for automatic, accurate screenshots of your app

Every app developer has to submit some kind of screenshot for potential users. This task can be tedious, difficult, time-consuming. Maybe you don't want to setup your app in certain ways so that it works, or looks a specific way.

Flutter is in all regards flexible, it offers great tooling for everything.

So what if, instead of writing tests, we want to create screenshots?

Laying down the foundation

First of all, taking screenshots automatically is not something Flutter has straight out of the box, so we are required to duct-tape some things together to make it work.

In my case, I have created a class called Bootstrapper which handles all the work of providing a "similar to production" environment.

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';

class Bootstrapper {
  final AppPreferences preferences;
  final String? locale;

  const Bootstrapper._(this.preferences, this.locale);

  static Future<Bootstrapper> getInstance(String? locale) async {
    // do asynchronous initialization things here

    // ignore: invalid_use_of_visible_for_testing_member
    SharedPreferences.setMockInitialValues({});
    final sharedPreferences = await SharedPreferences.getInstance();

    final preferences = AppPreferences(sharedPreferences);

    return Bootstrapper._(preferences, locale);
  }

  Widget wrap(Widget child) {
    return ProviderScope(
      overrides: [
        preferencesProvider.overrideWith((ref) => preferences),
      ],
      child: MaterialApp(
        home: child,
        debugShowCheckedModeBanner: false,
        useInheritedMediaQuery: true, // unsure if needed
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        locale: locale != null ? Locale(locale!) : const Locale("en"),
        theme: yourDefaultAppTheme,
      ),
    );
  }
}

With the Bootstrapper we cut down on writing the same code over and over.

Adding the necessary capturing code

Since Flutter does not provide an easy to use functions to capture screenshots, we need to write our own.

Future<Uint8List> takeScreenshot<T extends Widget>() async {
  final element = find.byType(T, skipOffstage: false).evaluate().first;
  final image = await captureImage(element);
  final data = await image.toByteData(format: ui.ImageByteFormat.png);
  return data!.buffer.asUint8List();
}

Future<ui.Image> captureImage(Element element) {
  assert(element.renderObject != null);
  var renderObject = element.renderObject!;
  while (!renderObject.isRepaintBoundary) {
    renderObject = renderObject.parent! as RenderObject;
  }
  assert(!renderObject.debugNeedsPaint);
  final layer = renderObject.debugLayer! as OffsetLayer;
  return layer.toImage(renderObject.paintBounds);
}

extension SetScreenSize on WidgetTester {
  Future<void> setScreenSize(Size size, [double pixelDensity = 1]) async {
    await binding.setSurfaceSize(size);
    binding.window.physicalSizeTestValue = size;
    binding.window.devicePixelRatioTestValue = pixelDensity;
  }
}

Writing your first test scene

Future<void> main() async {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  late Uint8List screenshot;
  
  testWidgets(
    'Main screen',
    (tester) async {
    
      // Setup
      await tester.setScreenSize(screenSize, screenDensity);
      final bootstrapper = await Bootstrapper.getInstance(locale);
      
      // Start our scene
      runApp(
        bootstrapper.wrap(const MainScreen()),
      );
      await tester.pumpAndSettle();
      
      // Take a screenshot
      screenshot = await takeScreenshot<MainScreen>();
    },
  );
  
  tearDownAll(() async {
    await File("screenshot.png").writeAsBytes(screenshot, flush: true);
  });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment