surf_widget_test_composer

Creator: coderz1093

Last updated:

0 purchases

surf_widget_test_composer Image
surf_widget_test_composer Images

Languages

Categories

Add to Cart

Description:

surf widget test composer

Widget Test Composer #


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






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$
copied to clipboard
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,
);
}
copied to clipboard
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);
},
);
}
copied to clipboard
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',
);
}
copied to clipboard

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);
},
);
}
copied to clipboard
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);
},
);
}
copied to clipboard
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);
},
);
}
copied to clipboard
Generating goldens #
Don't forget to generate goldens before use:
flutter test --update-goldens --tags=golden
copied to clipboard
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
copied to clipboard
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));
}
);
copied to clipboard

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,
)
copied to clipboard


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.

License #
Apache License, Version 2.0

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files In This Product:

Customer Reviews

There are no reviews.