surf_widget_test_composer 0.0.6 copy "surf_widget_test_composer: ^0.0.6" to clipboard
surf_widget_test_composer: ^0.0.6 copied to clipboard

Widget Test Composer is a utility package designed to simplify widget and golden testing processes using golden_toolkit package.

Widget Test Composer #

Made by Surf 🏄‍♂️🏄‍♂️🏄‍♂️

Build Status Coverage Status Pub Version Pub Likes Pub popularity License: Apache 2.0

Overview #

Widget Test Composer is a utility package designed to simplify widget and golden testing processes using golden_toolkit package for Flutter applications. Developed by Surf 🏄 Flutter team 🐄, it offers comprehensive features to facilitate efficient testing workflows.

Installation #

Add surf_widget_test_composer to your pubspec.yaml:

dependencies:
  surf_widget_test_composer: $currentVersion$

Example #

Getting started #

You need to create file test/flutter_test_config.dart. There you will specify:

  • localizations of your app;
  • themes of your app you need to test;
  • list of devices you want to test on;
  • tolerance for golden tests (the resulting diffPercent must be less than the tolerance settings property).

E.g. you have two themes: light and dark. You need to test the app on two devices: iPhone 11, Google Pixel 4a and iPhone SE 1. You need to test the app in two languages: English and Russian. You need to test your app on two locales: US and RU. You also have DI scope that you need to wrap your widget with.

Then your file test/flutter_test_config.dart will look like this:

import 'package:surf_widget_test_composer/surf_widget_test_composer.dart'
    as helper;

/// Localization and locales from auto-generated AppLocalizations.
const _localizations = AppLocalizations.localizationsDelegates;
const _locales = AppLocalizations.supportedLocales;

Future<void> testExecutable(FutureOr<void> Function() testMain) {
  /// You can specify your own themes.
  /// Stringified is used for naming screenshots.
  final themes = [
    helper.TestingTheme(
      data: ThemeData.dark(),
      stringified: 'dark',
      type: helper.ThemeType.dark,
    ),
    helper.TestingTheme(
      data: ThemeData.light(),
      stringified: 'light',
      type: helper.ThemeType.light,
    ),
  ];

  /// You can specify your own devices.
  final devices = [
    helper.TestDevice(
      name: 'iphone11',
      size: const Size(414, 896),
      safeArea: const EdgeInsets.only(top: 44, bottom: 34),
    ),
    helper.TestDevice(
      name: 'pixel 4a',
      size: const Size(393, 851),
    ),
    helper.TestDevice(
      name: 'iphone_se_1',
      size: const Size(640 / 2, 1136 / 2),
    ),
  ];

  return helper.testExecutable(
    testMain: testMain,
    themes: themes,
    localizations: _localizations,
    locales: _locales,
    wrapper: (child, mode, theme, localizations, locales) =>
        helper.BaseWidgetTestWrapper(
      childBuilder: child,
      mode: mode,
      themeData: theme,
      localizations: localizations,
      localeOverrides: locales,
      // You can specify dependencies here.
      dependencies: (child) => child,
    ),

    /// You can specify background color of golden test based on current theme.
    backgroundColor: (theme) => theme.colorScheme.background,
    devicesForTest: devices,

    /// You can specify tolerance for golden tests.
    tolerance: 0.5,
  );
}

According to the config, 12 goldens will be generated for each test: 2 locales x 2 themes x 3 devises.

For example goldens for SampleItemListView.

Usage #

Now we can prepare tests.

If in addition to golden tests you also need widget tests, then you can make something like this:

class MockSettingsService extends Mock implements SettingsService {}

void main() {
  final mockSettingsService = MockSettingsService();

  const widget = SettingsScreen();

  /// Generate golden.
  testWidget<SettingsScreen>(
    desc: 'SettingsScreen',
    widgetBuilder: (context, theme) => ProviderScope(
      overrides: [
        settingsServiceProvider.overrideWithValue(mockSettingsService),
      ],
      child: Consumer(
        builder: (context, ref, _) => widget.build(context, ref),
      ),
    ),
    setup: (context, mode) {
      registerFallbackValue(ThemeMode.light);

      when(() => mockSettingsService.themeMode()).thenAnswer(
        (_) => Future.value(ThemeMode.dark),
      );
      when(() => mockSettingsService.updateThemeMode(any()))
          .thenAnswer((_) => Future.value());
    },

    /// Widget tests.
    test: (tester, context) async {
      final button = find.byType(DropdownButton<ThemeMode>);
      expect(button, findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.light_mode);
      expect(floatingActionButton, findsOneWidget);

      verifyNever(() => mockSettingsService.updateThemeMode(any()));
      await tester.tap(floatingActionButton);
      verify(() => mockSettingsService.updateThemeMode(any())).called(1);
      await tester.pumpAndSettle();

      expect(find.byIcon(Icons.mode_night), findsOneWidget);
    },
  );
}

If you just need goldens, then the test might look like this:

void main() {
  const widget = SampleItemListView();

  /// Nothing to test, just want to generate the golden.
  testWidget<SampleItemListView>(
    desc: 'SampleItemListView - result',
    widgetBuilder: (context, _) => widget.build(context),
    // If we need to indicate that we are testing a specific widget/screen state,
    // we can fill in the [screenState] field.
    screenState: 'result',
  );
}

Warning

Always specify the generic type of the widget you are testing (e.g.,testWidget<TestableScreen>), as the golden's name generation is based on the widget class name.

Example for Elementary #

class MockElementaryCounterWM extends Mock implements IElementaryCounterWM {}

void main() {
  const int testValue = 5;
  const widget = ElementaryCounterScreen();
  final wm = MockElementaryCounterWM();

  /// Generate golden.
  testWidget<ElementaryCounterScreen>(
    desc: 'ElementaryCounterScreen',
    widgetBuilder: (context, theme) => widget.build(wm),
    setup: (context, mode) {
      when(() => wm.title).thenReturn('Elementary Counter');
      when(() => wm.value).thenReturn(StateNotifier<int>(initValue: testValue));
      when(() => wm.increment()).thenReturn(null);
    },

    /// Widget tests.
    test: (tester, context) async {
      expect(find.widgetWithText(Center, testValue.toString()), findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.add);
      expect(floatingActionButton, findsOneWidget);

      await tester.tap(floatingActionButton);
      verify(wm.increment);
    },
  );
}

Example for Riverpod #

class MockRiverpodCounterScreenController extends AutoDisposeNotifier<int>
    with Mock
    implements RiverpodCounterScreenController {}

void main() {
  const int testValue = 5;
  const widget = RiverpodCounterScreen();
  final mockController = MockRiverpodCounterScreenController();

  final container = ProviderContainer(
    overrides: [
      riverpodCounterScreenControllerProvider
          .overrideWith(() => mockController),
    ],
  );

  /// Generate golden.
  testWidget<RiverpodCounterScreen>(
    desc: 'RiverpodCounterScreen',
    widgetBuilder: (context, theme) => UncontrolledProviderScope(
      container: container,
      child: Consumer(
        builder: (context, ref, _) => widget.build(context, ref),
      ),
    ),

    setup: (context, mode) {
      when(() => mockController.build()).thenReturn(testValue);
      when(() => mockController.increment()).thenReturn(null);
    },

    /// Widget tests.
    test: (tester, context) async {
      expect(find.widgetWithText(Center, testValue.toString()), findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.add);
      expect(floatingActionButton, findsOneWidget);

      await tester.tap(floatingActionButton);
      verify(() => mockController.increment()).called(1);
    },
  );
}

Example for BLoC #

class MockBlocCounterBloc extends Mock implements BlocCounterBloc {}

void main() {
  const int testValue = 5;
  final mockBloc = MockBlocCounterBloc();
  const widget = BlocCounterView();

  /// Generate golden.
  testWidget<BlocCounterView>(
    desc: 'BlocCounterView',
    widgetBuilder: (context, theme) => MultiBlocProvider(
      providers: [
        BlocProvider<BlocCounterBloc>(create: (_) => mockBloc),
      ],
      child: widget,
    ),

    setup: (context, mode) {
      when(() => mockBloc.state).thenReturn(testValue);
      when(() => mockBloc.stream).thenAnswer(
        (_) => Stream<int>.fromIterable([testValue]),
      );
      when(() => mockBloc.add(Increment())).thenAnswer((_) => Future.value());
      when(() => mockBloc.close()).thenAnswer((_) => Future.value());
    },

    /// Widget tests.
    test: (tester, context) async {
      expect(find.widgetWithText(Center, testValue.toString()), findsOneWidget);

      final floatingActionButton = find.byIcon(Icons.add);
      expect(floatingActionButton, findsOneWidget);

      await tester.tap(floatingActionButton);
      verify(() => mockBloc.add(Increment())).called(1);
    },
  );
}

Generating goldens #

Don't forget to generate goldens before use:

flutter test --update-goldens --tags=golden

Additional Information #

While testing, you can face the following errors:

00:05 +0: WHEN tasks are not completedTHEN shows `CircularProgressIndicator`
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
pumpAndSettle timed out

This error means that the widget you are testing has an infinite loop. Usually this happens when you use looped animations. In order to fix this you can:

  • define your custom pump function. E.g.:
/// Nothing to test, just want to generate the golden.
  testWidget<TestableScreen>(
    'Test screen - loading',
    widgetBuilder: (_) => widget.build(wm),
    /// Since we are testing a specific widget state, we fill in the [screenState] property.
    screenState: 'loading',
    /// We define our custom pump function - golden will be generated after 100 milliseconds no matter animation is finished or not.
    customPump: (tester) => tester.pump(const Duration(milliseconds: 100)),
    setup: (context, data) {
      when(() => wm.data).thenReturn(EntityValueNotifier.loading());
      when(() => wm.theme).thenReturn(Theme.of(context));
    }
  );

NOTE: This may lead to a mismatch between same goldens - every time you run the test, the golden may be different.

  • you also can use TestEnvDetector.isTestEnvironment in your widget. E.g.:

    CircularProgressIndicator(
        value: TestEnvDetector.isTestEnvironment ? 0.5 : value,
        color: Colors.blue,
      )
    

Changelog #

All notable changes to this project will be documented in this file.

Issues #

To report your issues, submit them directly in the Issues section.

Contribute #

If you would like to contribute to the package (e.g. by improving the documentation, fixing a bug or adding a cool new feature), please read our contribution guide first and send us your pull request.

Your PRs are always welcome.

How to reach us #

Please feel free to ask any questions about this package. Join our community chat on Telegram. We speak English and Russian.

Telegram

License #

Apache License, Version 2.0

8
likes
140
pub points
71%
popularity

Publisher

verified publishersurf.ru

Widget Test Composer is a utility package designed to simplify widget and golden testing processes using golden_toolkit package.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

Apache-2.0 (LICENSE)

Dependencies

collection, flutter, flutter_test, golden_toolkit, meta

More

Packages that depend on surf_widget_test_composer